Skip to content

Commit e62559c

Browse files
committed
feat(database): unified certificate lookup table
Signed-off-by: Chris Gianelloni <[email protected]>
1 parent 0a2b68c commit e62559c

File tree

5 files changed

+514
-32
lines changed

5 files changed

+514
-32
lines changed

database/database_test.go

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@
1515
package database_test
1616

1717
import (
18+
"math/big"
1819
"testing"
1920
"time"
2021

2122
"github.com/blinklabs-io/dingo/database"
23+
"github.com/blinklabs-io/dingo/database/models"
24+
"github.com/blinklabs-io/gouroboros/cbor"
25+
lcommon "github.com/blinklabs-io/gouroboros/ledger/common"
26+
ocommon "github.com/blinklabs-io/gouroboros/protocol/common"
27+
"github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano"
2228
"gorm.io/gorm"
2329
)
2430

@@ -68,3 +74,317 @@ func TestInMemorySqliteMultipleTransaction(t *testing.T) {
6874
t.Fatalf("unexpected error: %s", err)
6975
}
7076
}
77+
78+
// mockTransaction implements lcommon.Transaction for testing
79+
type mockTransaction struct {
80+
hash lcommon.Blake2b256
81+
certificates []lcommon.Certificate
82+
isValid bool
83+
}
84+
85+
func (m *mockTransaction) Hash() lcommon.Blake2b256 {
86+
return m.hash
87+
}
88+
89+
func (m *mockTransaction) Id() lcommon.Blake2b256 {
90+
return m.hash
91+
}
92+
93+
func (m *mockTransaction) Type() int {
94+
return 0 // Shelley transaction
95+
}
96+
97+
func (m *mockTransaction) Fee() uint64 {
98+
return 1000
99+
}
100+
101+
func (m *mockTransaction) TTL() uint64 {
102+
return 1000000
103+
}
104+
105+
func (m *mockTransaction) IsValid() bool {
106+
return m.isValid
107+
}
108+
109+
func (m *mockTransaction) Metadata() lcommon.TransactionMetadatum {
110+
return nil
111+
}
112+
113+
func (m *mockTransaction) CollateralReturn() lcommon.TransactionOutput {
114+
return nil
115+
}
116+
117+
func (m *mockTransaction) Produced() []lcommon.Utxo {
118+
return nil
119+
}
120+
121+
func (m *mockTransaction) Outputs() []lcommon.TransactionOutput {
122+
return nil
123+
}
124+
125+
func (m *mockTransaction) Inputs() []lcommon.TransactionInput {
126+
return nil
127+
}
128+
129+
func (m *mockTransaction) Collateral() []lcommon.TransactionInput {
130+
return nil
131+
}
132+
133+
func (m *mockTransaction) Certificates() []lcommon.Certificate {
134+
return m.certificates
135+
}
136+
137+
func (m *mockTransaction) ProtocolParameterUpdates() (uint64, map[lcommon.Blake2b224]lcommon.ProtocolParameterUpdate) {
138+
return 0, nil
139+
}
140+
141+
func (m *mockTransaction) AssetMint() *lcommon.MultiAsset[lcommon.MultiAssetTypeMint] {
142+
return nil
143+
}
144+
145+
func (m *mockTransaction) AuxDataHash() *lcommon.Blake2b256 {
146+
return nil
147+
}
148+
149+
func (m *mockTransaction) Cbor() []byte {
150+
return []byte("mock_cbor")
151+
}
152+
153+
func (m *mockTransaction) Consumed() []lcommon.TransactionInput {
154+
return nil
155+
}
156+
157+
func (m *mockTransaction) Witnesses() lcommon.TransactionWitnessSet {
158+
return nil
159+
}
160+
161+
func (m *mockTransaction) ValidityIntervalStart() uint64 {
162+
return 0
163+
}
164+
165+
func (m *mockTransaction) ReferenceInputs() []lcommon.TransactionInput {
166+
return nil
167+
}
168+
169+
func (m *mockTransaction) TotalCollateral() uint64 {
170+
return 0
171+
}
172+
173+
func (m *mockTransaction) Withdrawals() map[*lcommon.Address]uint64 {
174+
return nil
175+
}
176+
177+
func (m *mockTransaction) RequiredSigners() []lcommon.Blake2b224 {
178+
return nil
179+
}
180+
181+
func (m *mockTransaction) ScriptDataHash() *lcommon.Blake2b256 {
182+
return nil
183+
}
184+
185+
func (m *mockTransaction) VotingProcedures() lcommon.VotingProcedures {
186+
return lcommon.VotingProcedures{}
187+
}
188+
189+
func (m *mockTransaction) ProposalProcedures() []lcommon.ProposalProcedure {
190+
return nil
191+
}
192+
193+
func (m *mockTransaction) CurrentTreasuryValue() int64 {
194+
return 0
195+
}
196+
197+
func (m *mockTransaction) Donation() uint64 {
198+
return 0
199+
}
200+
201+
func (m *mockTransaction) Utxorpc() (*cardano.Tx, error) {
202+
return nil, nil
203+
}
204+
205+
func (m *mockTransaction) LeiosHash() lcommon.Blake2b256 {
206+
return lcommon.Blake2b256{}
207+
}
208+
209+
// TestUnifiedCertificateCreation tests that unified certificate records are created
210+
// when processing transactions with certificates
211+
func TestUnifiedCertificateCreation(t *testing.T) {
212+
db, err := database.New(dbConfig)
213+
if err != nil {
214+
t.Fatalf("unexpected error: %s", err)
215+
}
216+
217+
// Run auto-migration to ensure tables exist
218+
if err := db.Metadata().DB().AutoMigrate(models.MigrateModels...); err != nil {
219+
t.Fatalf("failed to auto-migrate: %v", err)
220+
}
221+
222+
// Create a mock transaction with certificates
223+
mockTx := &mockTransaction{
224+
hash: lcommon.NewBlake2b256(
225+
[]byte("test_hash_1234567890123456789012345678901234567890"),
226+
),
227+
isValid: true,
228+
certificates: []lcommon.Certificate{
229+
&lcommon.StakeRegistrationCertificate{
230+
CertType: uint(lcommon.CertificateTypeStakeRegistration),
231+
StakeCredential: lcommon.Credential{
232+
CredType: lcommon.CredentialTypeAddrKeyHash,
233+
Credential: lcommon.CredentialHash(
234+
[]byte("stake_key_hash_1234567890123456789012345678"),
235+
),
236+
},
237+
},
238+
&lcommon.PoolRegistrationCertificate{
239+
CertType: uint(lcommon.CertificateTypePoolRegistration),
240+
Operator: lcommon.PoolKeyHash(
241+
[]byte("pool_key_hash_1234567890123456789012345678"),
242+
),
243+
VrfKeyHash: lcommon.VrfKeyHash(
244+
[]byte("vrf_key_hash_12345678901234567890123456789012"),
245+
),
246+
Pledge: 1000000,
247+
Cost: 340000000,
248+
Margin: cbor.Rat{Rat: big.NewRat(1, 100)},
249+
RewardAccount: lcommon.AddrKeyHash(
250+
[]byte("reward_account_1234567890123456789012345678"),
251+
),
252+
PoolOwners: []lcommon.AddrKeyHash{
253+
lcommon.AddrKeyHash(
254+
[]byte("owner1_1234567890123456789012345678"),
255+
),
256+
},
257+
},
258+
&lcommon.AuthCommitteeHotCertificate{
259+
CertType: uint(lcommon.CertificateTypeAuthCommitteeHot),
260+
ColdCredential: lcommon.Credential{
261+
CredType: lcommon.CredentialTypeAddrKeyHash,
262+
Credential: lcommon.CredentialHash(
263+
[]byte("cold_cred_hash_1234567890123456789012345678"),
264+
),
265+
},
266+
HotCredential: lcommon.Credential{
267+
CredType: lcommon.CredentialTypeAddrKeyHash,
268+
Credential: lcommon.CredentialHash(
269+
[]byte("hot_cred_hash_1234567890123456789012345678"),
270+
),
271+
},
272+
},
273+
},
274+
}
275+
276+
point := ocommon.Point{
277+
Hash: []byte("block_hash_12345678901234567890123456789012"),
278+
Slot: 1000000,
279+
}
280+
281+
// Process the transaction
282+
err = db.Metadata().
283+
SetTransaction(mockTx, point, 0, map[int]uint64{0: 2000000, 1: 500000000}, nil)
284+
if err != nil {
285+
t.Fatalf("failed to set transaction: %v", err)
286+
}
287+
288+
// Verify unified certificate records were created
289+
var unifiedCerts []models.Certificate
290+
if result := db.Metadata().DB().Order("cert_index ASC").Find(&unifiedCerts); result.Error != nil {
291+
t.Fatalf("failed to query unified certificates: %v", result.Error)
292+
}
293+
294+
if len(unifiedCerts) != 3 {
295+
t.Errorf("expected 3 unified certificates, got %d", len(unifiedCerts))
296+
}
297+
298+
// Verify the unified certificates have correct data
299+
for i, cert := range unifiedCerts {
300+
if cert.TransactionID == 0 {
301+
t.Errorf("certificate %d has zero transaction ID", i)
302+
}
303+
if cert.CertIndex != uint(i) {
304+
t.Errorf(
305+
"certificate %d has cert_index %d, expected %d",
306+
i,
307+
cert.CertIndex,
308+
i,
309+
)
310+
}
311+
if cert.Slot != point.Slot {
312+
t.Errorf(
313+
"certificate %d has slot %d, expected %d",
314+
i,
315+
cert.Slot,
316+
point.Slot,
317+
)
318+
}
319+
if string(cert.BlockHash) != string(point.Hash) {
320+
t.Errorf("certificate %d has wrong block hash", i)
321+
}
322+
}
323+
324+
// Verify specialized certificate records were created with correct CertificateID
325+
var stakeReg models.StakeRegistration
326+
if result := db.Metadata().DB().First(&stakeReg); result.Error != nil {
327+
t.Fatalf("failed to query stake registration: %v", result.Error)
328+
}
329+
330+
// Find the unified cert for stake registration (should be index 0)
331+
var stakeUnified models.Certificate
332+
if result := db.Metadata().DB().Where("cert_index = ? AND cert_type = ?", 0, uint(lcommon.CertificateTypeStakeRegistration)).First(&stakeUnified); result.Error != nil {
333+
t.Fatalf(
334+
"failed to find unified stake registration cert: %v",
335+
result.Error,
336+
)
337+
}
338+
339+
if stakeReg.CertificateID != stakeUnified.ID {
340+
t.Errorf(
341+
"stake registration CertificateID %d does not match unified cert ID %d",
342+
stakeReg.CertificateID,
343+
stakeUnified.ID,
344+
)
345+
}
346+
347+
var poolReg models.PoolRegistration
348+
if result := db.Metadata().DB().First(&poolReg); result.Error != nil {
349+
t.Fatalf("failed to query pool registration: %v", result.Error)
350+
}
351+
352+
// Find the unified cert for pool registration (should be index 1)
353+
var poolUnified models.Certificate
354+
if result := db.Metadata().DB().Where("cert_index = ? AND cert_type = ?", 1, uint(lcommon.CertificateTypePoolRegistration)).First(&poolUnified); result.Error != nil {
355+
t.Fatalf(
356+
"failed to find unified pool registration cert: %v",
357+
result.Error,
358+
)
359+
}
360+
361+
if poolReg.CertificateID != poolUnified.ID {
362+
t.Errorf(
363+
"pool registration CertificateID %d does not match unified cert ID %d",
364+
poolReg.CertificateID,
365+
poolUnified.ID,
366+
)
367+
}
368+
369+
var authHot models.AuthCommitteeHot
370+
if result := db.Metadata().DB().First(&authHot); result.Error != nil {
371+
t.Fatalf("failed to query auth committee hot: %v", result.Error)
372+
}
373+
374+
// Find the unified cert for auth committee hot (should be index 2)
375+
var authUnified models.Certificate
376+
if result := db.Metadata().DB().Where("cert_index = ? AND cert_type = ?", 2, uint(lcommon.CertificateTypeAuthCommitteeHot)).First(&authUnified); result.Error != nil {
377+
t.Fatalf(
378+
"failed to find unified auth committee hot cert: %v",
379+
result.Error,
380+
)
381+
}
382+
383+
if authHot.CertificateID != authUnified.ID {
384+
t.Errorf(
385+
"auth committee hot CertificateID %d does not match unified cert ID %d",
386+
authHot.CertificateID,
387+
authUnified.ID,
388+
)
389+
}
390+
}

database/models/auth_committee_hot.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type AuthCommitteeHot struct {
1818
ColdCredential []byte `gorm:"index"`
1919
HostCredential []byte `gorm:"index"`
2020
ID uint `gorm:"primarykey"`
21+
CertificateID uint `gorm:"index"`
2122
AddedSlot uint64
2223
}
2324

database/models/certs.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,19 @@
1414

1515
package models
1616

17-
import "github.com/blinklabs-io/dingo/database/types"
18-
17+
// Certificate maps transaction certificates to their specialized table records.
18+
// Provides unified indexing across all certificate types without requiring joins.
19+
//
20+
// All certificate types now have dedicated specialized models. The CertificateID field
21+
// references the ID of the specific certificate record based on CertType.
1922
type Certificate struct {
20-
Cbor []byte `gorm:"-"`
21-
Pool []byte
22-
Credential []byte
23-
Drep []byte
24-
CertType uint `gorm:"index"`
25-
Epoch uint64
26-
Amount types.Uint64
27-
ID uint `gorm:"primaryKey"`
23+
BlockHash []byte `gorm:"index"`
24+
ID uint `gorm:"primaryKey"`
25+
TransactionID uint `gorm:"index;uniqueIndex:uniq_tx_cert;constraint:OnDelete:CASCADE"`
26+
CertificateID uint `gorm:"index"` // Polymorphic FK to certificate table based on CertType. Not DB-enforced.
27+
Slot uint64 `gorm:"index"`
28+
CertIndex uint `gorm:"column:cert_index;uniqueIndex:uniq_tx_cert"`
29+
CertType uint `gorm:"index"`
2830
}
2931

3032
func (Certificate) TableName() string {

database/models/resign_committee_cold.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type ResignCommitteeCold struct {
1919
ColdCredential []byte `gorm:"index"`
2020
AnchorHash []byte
2121
ID uint `gorm:"primarykey"`
22+
CertificateID uint `gorm:"index"`
2223
AddedSlot uint64
2324
}
2425

0 commit comments

Comments
 (0)