Skip to content

Commit 5221bed

Browse files
authored
feat(adder): DRep Registrations and Retiring (#615)
* feat(adder): Added events for drep registration, update and deregistration certificates Signed-off-by: Akhil Repala <arepala@blinklabs.io> * feat(adder): Fixed drep bech32 prefix for script-hash certificates Signed-off-by: Akhil Repala <arepala@blinklabs.io> --------- Signed-off-by: Akhil Repala <arepala@blinklabs.io>
1 parent f6d7094 commit 5221bed

File tree

5 files changed

+253
-39
lines changed

5 files changed

+253
-39
lines changed

event/drep.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 event
16+
17+
import "github.com/blinklabs-io/gouroboros/ledger"
18+
19+
const (
20+
DRepCertificateTypeRegistration = "Registration"
21+
DRepCertificateTypeUpdate = "Update"
22+
DRepCertificateTypeDeregistration = "Deregistration"
23+
)
24+
25+
const (
26+
DRepRegistrationEventType = "chainsync.drep.registration"
27+
DRepUpdateEventType = "chainsync.drep.update"
28+
DRepDeregistrationEventType = "chainsync.drep.deregistration"
29+
)
30+
31+
// DRepCertificateEvent represents a single DRep certificate event
32+
type DRepCertificateEvent struct {
33+
BlockHash string `json:"blockHash"`
34+
Certificate DRepCertificateData `json:"certificate"`
35+
}
36+
37+
// NewDRepCertificateEvent creates a new DRepCertificateEvent
38+
func NewDRepCertificateEvent(
39+
block ledger.Block,
40+
cert DRepCertificateData,
41+
) DRepCertificateEvent {
42+
return DRepCertificateEvent{
43+
BlockHash: block.Hash().String(),
44+
Certificate: cert,
45+
}
46+
}
47+
48+
// DRepEventType returns the event type for the given certificate type
49+
func DRepEventType(certType string) (string, bool) {
50+
switch certType {
51+
case DRepCertificateTypeRegistration:
52+
return DRepRegistrationEventType, true
53+
case DRepCertificateTypeUpdate:
54+
return DRepUpdateEventType, true
55+
case DRepCertificateTypeDeregistration:
56+
return DRepDeregistrationEventType, true
57+
default:
58+
return "", false
59+
}
60+
}

event/governance.go

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -412,46 +412,11 @@ func extractGovernanceCertificates(tx ledger.Transaction) (
412412
committee []CommitteeCertificateData,
413413
) {
414414
for _, cert := range tx.Certificates() {
415-
switch c := cert.(type) {
416-
// DRep certificates
417-
case *lcommon.RegistrationDrepCertificate:
418-
data := DRepCertificateData{
419-
CertificateType: "Registration",
420-
DRepHash: hex.EncodeToString(c.DrepCredential.Hash().Bytes()),
421-
DRepId: c.DrepCredential.Hash().Bech32("drep"),
422-
Deposit: c.Amount,
423-
}
424-
// Certificate anchors are pointer types (*GovAnchor), so we check for nil
425-
if c.Anchor != nil {
426-
data.Anchor = AnchorData{
427-
Url: c.Anchor.Url,
428-
DataHash: hex.EncodeToString(c.Anchor.DataHash[:]),
429-
}
430-
}
415+
if data, ok := extractDRepCertificate(cert); ok {
431416
drep = append(drep, data)
432-
433-
case *lcommon.DeregistrationDrepCertificate:
434-
drep = append(drep, DRepCertificateData{
435-
CertificateType: "Deregistration",
436-
DRepHash: hex.EncodeToString(c.DrepCredential.Hash().Bytes()),
437-
DRepId: c.DrepCredential.Hash().Bech32("drep"),
438-
Deposit: c.Amount,
439-
})
440-
441-
case *lcommon.UpdateDrepCertificate:
442-
data := DRepCertificateData{
443-
CertificateType: "Update",
444-
DRepHash: hex.EncodeToString(c.DrepCredential.Hash().Bytes()),
445-
DRepId: c.DrepCredential.Hash().Bech32("drep"),
446-
}
447-
if c.Anchor != nil {
448-
data.Anchor = AnchorData{
449-
Url: c.Anchor.Url,
450-
DataHash: hex.EncodeToString(c.Anchor.DataHash[:]),
451-
}
452-
}
453-
drep = append(drep, data)
454-
417+
continue
418+
}
419+
switch c := cert.(type) {
455420
// Vote delegation certificates
456421
case *lcommon.VoteDelegationCertificate:
457422
voteDel = append(voteDel, extractVoteDelegation("VoteDelegation", c.StakeCredential, c.Drep, 0))
@@ -494,6 +459,75 @@ func extractGovernanceCertificates(tx ledger.Transaction) (
494459
return drep, voteDel, committee
495460
}
496461

462+
// ExtractDRepCertificates extracts DRep certificates from a transaction
463+
func ExtractDRepCertificates(tx ledger.Transaction) []DRepCertificateData {
464+
if len(tx.Certificates()) == 0 {
465+
return nil
466+
}
467+
468+
var result []DRepCertificateData
469+
for _, cert := range tx.Certificates() {
470+
if data, ok := extractDRepCertificate(cert); ok {
471+
result = append(result, data)
472+
}
473+
}
474+
if len(result) == 0 {
475+
return nil
476+
}
477+
return result
478+
}
479+
480+
func drepBech32Prefix(cred lcommon.Credential) string {
481+
if cred.CredType == 1 {
482+
return "drep_script"
483+
}
484+
return "drep"
485+
}
486+
487+
func extractDRepCertificate(cert ledger.Certificate) (DRepCertificateData, bool) {
488+
switch c := cert.(type) {
489+
case *lcommon.RegistrationDrepCertificate:
490+
data := DRepCertificateData{
491+
CertificateType: DRepCertificateTypeRegistration,
492+
DRepHash: hex.EncodeToString(c.DrepCredential.Hash().Bytes()),
493+
DRepId: c.DrepCredential.Hash().Bech32(drepBech32Prefix(c.DrepCredential)),
494+
Deposit: c.Amount,
495+
}
496+
// Certificate anchors are pointer types (*GovAnchor), so we check for nil
497+
if c.Anchor != nil {
498+
data.Anchor = AnchorData{
499+
Url: c.Anchor.Url,
500+
DataHash: hex.EncodeToString(c.Anchor.DataHash[:]),
501+
}
502+
}
503+
return data, true
504+
505+
case *lcommon.DeregistrationDrepCertificate:
506+
return DRepCertificateData{
507+
CertificateType: DRepCertificateTypeDeregistration,
508+
DRepHash: hex.EncodeToString(c.DrepCredential.Hash().Bytes()),
509+
DRepId: c.DrepCredential.Hash().Bech32(drepBech32Prefix(c.DrepCredential)),
510+
Deposit: c.Amount,
511+
}, true
512+
513+
case *lcommon.UpdateDrepCertificate:
514+
data := DRepCertificateData{
515+
CertificateType: DRepCertificateTypeUpdate,
516+
DRepHash: hex.EncodeToString(c.DrepCredential.Hash().Bytes()),
517+
DRepId: c.DrepCredential.Hash().Bech32(drepBech32Prefix(c.DrepCredential)),
518+
}
519+
if c.Anchor != nil {
520+
data.Anchor = AnchorData{
521+
Url: c.Anchor.Url,
522+
DataHash: hex.EncodeToString(c.Anchor.DataHash[:]),
523+
}
524+
}
525+
return data, true
526+
}
527+
528+
return DRepCertificateData{}, false
529+
}
530+
497531
// Helper functions
498532

499533
func getGovActionType(action lcommon.GovAction) string {

filter/cardano/cardano.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ func (c *Cardano) filterEvent(evt event.Event) bool {
9090
return c.filterTransactionEvent(v)
9191
case event.GovernanceEvent:
9292
return c.filterGovernanceEvent(v)
93+
case event.DRepCertificateEvent:
94+
return c.filterDRepCertificateEvent(v)
9395
default:
9496
// Pass through events we don't filter
9597
return true
@@ -161,6 +163,23 @@ func (c *Cardano) filterTransactionEvent(te event.TransactionEvent) bool {
161163
return true
162164
}
163165

166+
// filterDRepCertificateEvent checks DRep filter for DRep certificate events
167+
func (c *Cardano) filterDRepCertificateEvent(de event.DRepCertificateEvent) bool {
168+
if !c.filterSet.hasDRepFilter {
169+
return true
170+
}
171+
172+
if _, exists := c.filterSet.dreps.hexDRepIds[de.Certificate.DRepHash]; exists {
173+
return true
174+
}
175+
176+
if _, exists := c.filterSet.dreps.bech32DRepIds[de.Certificate.DRepId]; exists {
177+
return true
178+
}
179+
180+
return false
181+
}
182+
164183
// matchAddressFilter checks if transaction matches address filters
165184
func (c *Cardano) matchAddressFilter(te event.TransactionEvent) bool {
166185
// Include resolved inputs as outputs for matching

filter/cardano/cardano_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,65 @@ func TestFilterByDRepIdGovernanceEvent(t *testing.T) {
682682
})
683683
}
684684

685+
func TestFilterByDRepIdDRepCertificateEvent(t *testing.T) {
686+
// 28 bytes = 56 hex chars for Blake2b224
687+
drepHex := "abcd1234567890abcdef1234567890abcdef1234567890abcdef1234"
688+
689+
t.Run("matches DRep certificate event", func(t *testing.T) {
690+
cs := New(WithDRepIds([]string{drepHex}))
691+
692+
evt := event.Event{
693+
Payload: event.DRepCertificateEvent{
694+
Certificate: event.DRepCertificateData{
695+
CertificateType: event.DRepCertificateTypeRegistration,
696+
DRepHash: drepHex,
697+
DRepId: "drep1...",
698+
},
699+
},
700+
}
701+
702+
err := cs.Start()
703+
assert.NoError(t, err)
704+
defer cs.Stop()
705+
706+
cs.InputChan() <- evt
707+
708+
select {
709+
case filteredEvt := <-cs.OutputChan():
710+
assert.Equal(t, evt, filteredEvt)
711+
case <-time.After(1 * time.Second):
712+
t.Error("Expected event to pass filter but it didn't")
713+
}
714+
})
715+
716+
t.Run("does not match when DRep not in filter", func(t *testing.T) {
717+
cs := New(WithDRepIds([]string{drepHex}))
718+
719+
evt := event.Event{
720+
Payload: event.DRepCertificateEvent{
721+
Certificate: event.DRepCertificateData{
722+
CertificateType: event.DRepCertificateTypeRegistration,
723+
DRepHash: "deadbeef",
724+
DRepId: "drep1notmatched",
725+
},
726+
},
727+
}
728+
729+
err := cs.Start()
730+
assert.NoError(t, err)
731+
defer cs.Stop()
732+
733+
cs.InputChan() <- evt
734+
735+
select {
736+
case <-cs.OutputChan():
737+
t.Error("Expected event to be filtered out but it passed through")
738+
case <-time.After(100 * time.Millisecond):
739+
// Expected no event
740+
}
741+
})
742+
}
743+
685744
func TestWithDRepIds(t *testing.T) {
686745
t.Run("stores bech32 drep ID and computes hex", func(t *testing.T) {
687746
// Create a valid drep bech32 ID from known hex (28 bytes = 56 hex chars)

input/chainsync/chainsync.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,27 @@ func (c *ChainSync) handleRollForward(
523523
)
524524
tmpEvents = append(tmpEvents, govEvt)
525525
}
526+
// Emit DRep certificate events
527+
if drepCerts := event.ExtractDRepCertificates(transaction); len(drepCerts) > 0 {
528+
drepCtx := event.NewGovernanceContext(
529+
block,
530+
transaction,
531+
//nolint:gosec // t is bounds-checked above
532+
uint32(t),
533+
c.networkMagic,
534+
)
535+
for _, cert := range drepCerts {
536+
if evtType, ok := event.DRepEventType(cert.CertificateType); ok {
537+
drepEvt := event.New(
538+
evtType,
539+
time.Now(),
540+
drepCtx,
541+
event.NewDRepCertificateEvent(block, cert),
542+
)
543+
tmpEvents = append(tmpEvents, drepEvt)
544+
}
545+
}
546+
}
526547
}
527548
updateTip := ochainsync.Tip{
528549
Point: ocommon.Point{
@@ -630,6 +651,27 @@ func (c *ChainSync) handleBlockFetchBlock(
630651
)
631652
c.eventChan <- govEvt
632653
}
654+
// Emit DRep certificate events
655+
if drepCerts := event.ExtractDRepCertificates(transaction); len(drepCerts) > 0 {
656+
drepCtx := event.NewGovernanceContext(
657+
block,
658+
transaction,
659+
//nolint:gosec // t is bounds-checked above
660+
uint32(t),
661+
c.networkMagic,
662+
)
663+
for _, cert := range drepCerts {
664+
if evtType, ok := event.DRepEventType(cert.CertificateType); ok {
665+
drepEvt := event.New(
666+
evtType,
667+
time.Now(),
668+
drepCtx,
669+
event.NewDRepCertificateEvent(block, cert),
670+
)
671+
c.eventChan <- drepEvt
672+
}
673+
}
674+
}
633675
}
634676
c.updateStatus(
635677
block.SlotNumber(),

0 commit comments

Comments
 (0)