Skip to content

Commit 6275fdc

Browse files
authored
refactor(database): remove direct utxo manipulation (#975)
Rather than directly manipulating UTxOs from the ledger, we process transactions. This moves the specifics of UTxO processing to the database package. Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent f7ebf24 commit 6275fdc

File tree

11 files changed

+228
-352
lines changed

11 files changed

+228
-352
lines changed

database/block.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,13 @@ func BlockByPointTxn(txn *Txn, point ocommon.Point) (models.Block, error) {
157157
return blockByKey(txn, key)
158158
}
159159

160-
func (d *Database) BlockByIndex(blockIndex uint64, txn *Txn) (models.Block, error) {
160+
func (d *Database) BlockByIndex(
161+
blockIndex uint64,
162+
txn *Txn,
163+
) (models.Block, error) {
161164
if txn == nil {
162165
txn = d.BlobTxn(false)
163-
defer txn.Commit() //nolint:errcheck
166+
defer txn.Rollback() //nolint:errcheck
164167
}
165168
indexKey := BlockBlobIndexKey(blockIndex)
166169
item, err := txn.Blob().Get(indexKey)

database/models/transaction.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ package models
1616

1717
// Transaction represents a transaction record
1818
type Transaction struct {
19-
Type string
2019
Hash []byte `gorm:"uniqueIndex"`
2120
BlockHash []byte `gorm:"index"`
22-
Inputs []byte
23-
Outputs []byte
24-
ID uint `gorm:"primaryKey"`
21+
Inputs []Utxo `gorm:"foreignKey:SpentAtTxId;references:Hash"`
22+
Outputs []Utxo `gorm:"foreignKey:TransactionID;references:ID"`
23+
ID uint `gorm:"primaryKey"`
24+
Type int
2525
BlockIndex uint32
2626
}
2727

database/models/utxo.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package models
1616

1717
import (
1818
"github.com/blinklabs-io/gouroboros/ledger"
19+
lcommon "github.com/blinklabs-io/gouroboros/ledger/common"
1920
)
2021

2122
// Utxo represents an unspent transaction output
@@ -26,6 +27,7 @@ type Utxo struct {
2627
StakingKey []byte `gorm:"index"`
2728
Assets []Asset
2829
Cbor []byte `gorm:"-"` // This is here for convenience but not represented in the metadata DB
30+
SpentAtTxId []byte `gorm:"index"`
2931
ID uint `gorm:"primarykey"`
3032
AddedSlot uint64 `gorm:"index"`
3133
DeletedSlot uint64 `gorm:"index"`
@@ -41,6 +43,37 @@ func (u *Utxo) Decode() (ledger.TransactionOutput, error) {
4143
return ledger.NewTransactionOutputFromCbor(u.Cbor)
4244
}
4345

46+
func UtxoLedgerToModel(
47+
utxo ledger.Utxo,
48+
slot uint64,
49+
) Utxo {
50+
outAddr := utxo.Output.Address()
51+
ret := Utxo{
52+
TxId: utxo.Id.Id().Bytes(),
53+
Cbor: utxo.Output.Cbor(),
54+
AddedSlot: slot,
55+
Amount: utxo.Output.Amount(),
56+
OutputIdx: utxo.Id.Index(),
57+
}
58+
pkh := outAddr.PaymentKeyHash()
59+
if pkh != ledger.NewBlake2b224(nil) {
60+
ret.PaymentKey = pkh.Bytes()
61+
}
62+
skh := outAddr.StakeKeyHash()
63+
if skh != ledger.NewBlake2b224(nil) {
64+
ret.StakingKey = skh.Bytes()
65+
}
66+
if multiAssetOutput, ok := utxo.Output.(interface {
67+
MultiAsset() *lcommon.MultiAsset[lcommon.MultiAssetTypeOutput]
68+
}); ok {
69+
if multiAsset := multiAssetOutput.MultiAsset(); multiAsset != nil {
70+
ret.Assets = ConvertMultiAssetToModels(multiAsset)
71+
}
72+
}
73+
74+
return ret
75+
}
76+
4477
// UtxoSlot allows providing a slot number with a ledger.Utxo object
4578
type UtxoSlot struct {
4679
Utxo ledger.Utxo

database/plugin/metadata/sqlite/transaction.go

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ package sqlite
1616

1717
import (
1818
"errors"
19+
"fmt"
1920

2021
"github.com/blinklabs-io/dingo/database/models"
22+
lcommon "github.com/blinklabs-io/gouroboros/ledger/common"
23+
ocommon "github.com/blinklabs-io/gouroboros/protocol/common"
2124
"gorm.io/gorm"
25+
"gorm.io/gorm/clause"
2226
)
2327

2428
// GetTransactionByHash returns a transaction by its hash
@@ -42,26 +46,78 @@ func (d *MetadataStoreSqlite) GetTransactionByHash(
4246

4347
// SetTransaction adds a new transaction to the database
4448
func (d *MetadataStoreSqlite) SetTransaction(
45-
hash []byte,
46-
txType string,
47-
blockHash []byte,
48-
blockIndex uint32,
49-
inputs []byte,
50-
outputs []byte,
49+
tx lcommon.Transaction,
50+
point ocommon.Point,
51+
idx uint32,
5152
txn *gorm.DB,
5253
) error {
5354
if txn == nil {
5455
txn = d.DB()
5556
}
56-
tx := &models.Transaction{
57-
Hash: hash,
58-
Type: txType,
59-
BlockHash: blockHash,
60-
BlockIndex: blockIndex,
61-
Inputs: inputs,
62-
Outputs: outputs,
57+
tmpTx := &models.Transaction{
58+
Hash: tx.Hash().Bytes(),
59+
Type: tx.Type(),
60+
BlockHash: point.Hash,
61+
BlockIndex: idx,
62+
}
63+
if tx.IsValid() {
64+
for _, utxo := range tx.Produced() {
65+
tmpTx.Outputs = append(
66+
tmpTx.Outputs,
67+
models.UtxoLedgerToModel(utxo, point.Slot),
68+
)
69+
}
70+
}
71+
result := txn.Create(&tmpTx)
72+
if result.Error != nil {
73+
return fmt.Errorf("create transaction: %w", result.Error)
74+
}
75+
// Explicitly create produced outputs with TransactionID set
76+
if tx.IsValid() && len(tmpTx.Outputs) > 0 {
77+
for o := range tmpTx.Outputs {
78+
tmpTx.Outputs[o].TransactionID = &tmpTx.ID
79+
}
80+
result = txn.Clauses(clause.OnConflict{
81+
UpdateAll: true,
82+
}).Create(&tmpTx.Outputs)
83+
if result.Error != nil {
84+
return fmt.Errorf("create outputs: %w", result.Error)
85+
}
86+
}
87+
for i, input := range tx.Consumed() {
88+
inTxId := input.Id().Bytes()
89+
inIdx := input.Index()
90+
utxo, err := d.GetUtxo(inTxId, inIdx, txn)
91+
if err != nil {
92+
return fmt.Errorf(
93+
"failed to fetch input %x#%d: %w",
94+
inTxId,
95+
inIdx,
96+
err,
97+
)
98+
}
99+
if utxo == nil {
100+
return fmt.Errorf("input UTxO not found: %x#%d", inTxId, inIdx)
101+
}
102+
// Update existing UTxOs
103+
result = txn.Model(&models.Utxo{}).
104+
Where("tx_id = ? AND output_idx = ?", inTxId, inIdx).
105+
Updates(map[string]any{
106+
"deleted_slot": point.Slot,
107+
"spent_at_tx_id": tx.Hash().Bytes(),
108+
})
109+
if result.Error != nil {
110+
return result.Error
111+
}
112+
// Explicitly set consumed inputs with TransactionID set
113+
tmpTx.Inputs = append(
114+
tmpTx.Inputs,
115+
*utxo,
116+
)
117+
tmpTx.Inputs[i].TransactionID = &utxo.ID
63118
}
64-
result := txn.Create(&tx)
119+
// Avoid updating associations
120+
result = txn.Omit(clause.Associations).Save(&tmpTx.Inputs)
65121
if result.Error != nil {
66122
return result.Error
67123
}

database/plugin/metadata/sqlite/utxo.go

Lines changed: 14 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020

2121
"github.com/blinklabs-io/dingo/database/models"
2222
"github.com/blinklabs-io/gouroboros/ledger"
23-
lcommon "github.com/blinklabs-io/gouroboros/ledger/common"
2423
"gorm.io/gorm"
2524
)
2625

@@ -102,6 +101,9 @@ func (d *MetadataStoreSqlite) GetUtxosByAddress(
102101
var ret []models.Utxo
103102
// Build sub-query for address
104103
var addrQuery *gorm.DB
104+
if txn == nil {
105+
txn = d.DB()
106+
}
105107
if addr.PaymentKeyHash() != ledger.NewBlake2b224(nil) {
106108
addrQuery = txn.Where("payment_key = ?", addr.PaymentKeyHash().Bytes())
107109
}
@@ -115,6 +117,9 @@ func (d *MetadataStoreSqlite) GetUtxosByAddress(
115117
addrQuery = txn.Where("staking_key = ?", addr.StakeKeyHash().Bytes())
116118
}
117119
}
120+
if addrQuery == nil {
121+
return ret, nil
122+
}
118123
result := txn.
119124
Where("deleted_slot = 0").
120125
Where(addrQuery).
@@ -193,43 +198,6 @@ func (d *MetadataStoreSqlite) DeleteUtxosAfterSlot(
193198
return nil
194199
}
195200

196-
// SetUtxo saves a UTxO
197-
func (d *MetadataStoreSqlite) SetUtxo(
198-
txId []byte, // hash
199-
idx uint32, // idx
200-
slot uint64, // slot
201-
payment []byte, // payment
202-
stake []byte, // stake
203-
amount uint64, // amount
204-
assets *lcommon.MultiAsset[lcommon.MultiAssetTypeOutput], // assets (multi-asset)
205-
txn *gorm.DB,
206-
) error {
207-
tmpUtxo := models.Utxo{
208-
TxId: txId,
209-
OutputIdx: idx,
210-
AddedSlot: slot,
211-
PaymentKey: payment,
212-
StakingKey: stake,
213-
}
214-
215-
// Handle assets if present
216-
if assets != nil {
217-
tmpUtxo.Assets = models.ConvertMultiAssetToModels(assets)
218-
}
219-
220-
var result *gorm.DB
221-
if txn != nil {
222-
result = txn.Create(&tmpUtxo)
223-
} else {
224-
result = d.DB().Create(&tmpUtxo)
225-
}
226-
227-
if result.Error != nil {
228-
return result.Error
229-
}
230-
return nil
231-
}
232-
233201
// AddUtxos saves a batch of UTxOs
234202
func (d *MetadataStoreSqlite) AddUtxos(
235203
utxos []models.UtxoSlot,
@@ -239,19 +207,16 @@ func (d *MetadataStoreSqlite) AddUtxos(
239207
for _, utxo := range utxos {
240208
items = append(
241209
items,
242-
utxoLedgerToModel(utxo.Utxo, utxo.Slot),
210+
models.UtxoLedgerToModel(utxo.Utxo, utxo.Slot),
243211
)
244212
}
245-
if txn != nil {
246-
result := txn.Create(items)
247-
if result.Error != nil {
248-
return result.Error
249-
}
250-
} else {
251-
result := d.DB().Create(items)
252-
if result.Error != nil {
253-
return result.Error
254-
}
213+
if txn == nil {
214+
txn = d.DB()
215+
}
216+
result := txn.Session(&gorm.Session{FullSaveAssociations: true}).
217+
CreateInBatches(items, 1000)
218+
if result.Error != nil {
219+
return result.Error
255220
}
256221
return nil
257222
}
@@ -302,28 +267,3 @@ func (d *MetadataStoreSqlite) SetUtxosNotDeletedAfterSlot(
302267
}
303268
return nil
304269
}
305-
306-
func utxoLedgerToModel(
307-
utxo ledger.Utxo,
308-
slot uint64,
309-
) models.Utxo {
310-
outAddr := utxo.Output.Address()
311-
ret := models.Utxo{
312-
TxId: utxo.Id.Id().Bytes(),
313-
OutputIdx: utxo.Id.Index(),
314-
AddedSlot: slot,
315-
PaymentKey: outAddr.PaymentKeyHash().Bytes(),
316-
StakingKey: outAddr.StakeKeyHash().Bytes(),
317-
Cbor: utxo.Output.Cbor(),
318-
}
319-
320-
if multiAssetOutput, ok := utxo.Output.(interface {
321-
MultiAsset() *lcommon.MultiAsset[lcommon.MultiAssetTypeOutput]
322-
}); ok {
323-
if multiAsset := multiAssetOutput.MultiAsset(); multiAsset != nil {
324-
ret.Assets = models.ConvertMultiAssetToModels(multiAsset)
325-
}
326-
}
327-
328-
return ret
329-
}

database/plugin/metadata/store.go

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/blinklabs-io/gouroboros/ledger"
2323
lcommon "github.com/blinklabs-io/gouroboros/ledger/common"
2424
ochainsync "github.com/blinklabs-io/gouroboros/protocol/chainsync"
25+
ocommon "github.com/blinklabs-io/gouroboros/protocol/common"
2526
"github.com/prometheus/client_golang/prometheus"
2627
"gorm.io/gorm"
2728
)
@@ -187,32 +188,19 @@ type MetadataStore interface {
187188
uint64, // deposit
188189
*gorm.DB,
189190
) error
190-
SetTransaction(
191-
[]byte, // hash
192-
string, // type
193-
[]byte, // blockHash
194-
uint32, // blockIndex
195-
[]byte, // inputs
196-
[]byte, // outputs
197-
*gorm.DB,
198-
) error
199191
SetTip(
200192
ochainsync.Tip,
201193
*gorm.DB,
202194
) error
203-
SetUpdateDrep(
204-
*lcommon.UpdateDrepCertificate,
205-
uint64, // slot
195+
SetTransaction(
196+
lcommon.Transaction,
197+
ocommon.Point,
198+
uint32, // idx
206199
*gorm.DB,
207200
) error
208-
SetUtxo(
209-
[]byte, // hash
210-
uint32, // idx
201+
SetUpdateDrep(
202+
*lcommon.UpdateDrepCertificate,
211203
uint64, // slot
212-
[]byte, // payment
213-
[]byte, // stake
214-
uint64, // amount
215-
*lcommon.MultiAsset[lcommon.MultiAssetTypeOutput], // asset
216204
*gorm.DB,
217205
) error
218206
SetVoteDelegation(

0 commit comments

Comments
 (0)