Skip to content

Commit 47f246a

Browse files
chrisguineyclaude
andauthored
fix(ledger): parse genesis staking pool vrf/owners/metadata fields (#1859)
PoolRegistrationCertificate.UnmarshalJSON read only the on-chain/API field names (vrfKeyHash, poolOwners, poolMetadata, operator), but Shelley genesis staking.pools uses vrf, owners, metadata, and publicKey for the same values. As a result a pool declared in genesis parsed with an all-zeros VRF key hash (and dropped owners and metadata). A zero VRF key hash breaks consensus header VRF-key validation: the pool's blocks are rejected because their real VRF key does not match the zero registered value. Accept both name sets, preferring the on-chain/API name when both are present. Assert the parsed VRF key hash in the InitialPools/PoolById tests, which previously only checked cost, so the field-name mismatch went unnoticed. Signed-off-by: Chris Guiney <chris@guiney.net> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9adf670 commit 47f246a

2 files changed

Lines changed: 61 additions & 8 deletions

File tree

ledger/common/certs.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,16 @@ func (p *PoolRegistrationCertificate) UnmarshalJSON(data []byte) error {
525525
Hostname *string `json:"hostname,omitempty"`
526526
} `json:"relays"`
527527
PoolMetadata *PoolMetadata `json:"poolMetadata,omitempty"`
528+
529+
// Shelley genesis staking.pools uses different field names than the
530+
// on-chain / API representation above for the same values. Accept both
531+
// so pools declared in genesis parse correctly — otherwise a genesis
532+
// pool's VRF key hash is dropped and reads as all-zeros, which breaks
533+
// header VRF-key validation for that pool's blocks.
534+
PublicKey string `json:"publicKey"`
535+
Vrf string `json:"vrf"`
536+
Owners []string `json:"owners"`
537+
Metadata *PoolMetadata `json:"metadata,omitempty"`
528538
}
529539

530540
var tmp tempPool
@@ -533,6 +543,25 @@ func (p *PoolRegistrationCertificate) UnmarshalJSON(data []byte) error {
533543
return fmt.Errorf("failed to unmarshal pool registration: %w", err)
534544
}
535545

546+
// Resolve fields that have distinct genesis-staking names, preferring the
547+
// on-chain/API name when both are present.
548+
operator := tmp.Operator
549+
if operator == "" {
550+
operator = tmp.PublicKey
551+
}
552+
vrfKeyHash := tmp.VrfKeyHash
553+
if vrfKeyHash == "" {
554+
vrfKeyHash = tmp.Vrf
555+
}
556+
poolOwners := tmp.PoolOwners
557+
if len(poolOwners) == 0 {
558+
poolOwners = tmp.Owners
559+
}
560+
poolMetadata := tmp.PoolMetadata
561+
if poolMetadata == nil {
562+
poolMetadata = tmp.Metadata
563+
}
564+
536565
p.Pledge = tmp.Pledge
537566
p.Cost = tmp.Cost
538567
p.Relays = make([]PoolRelay, len(tmp.Relays))
@@ -545,7 +574,7 @@ func (p *PoolRegistrationCertificate) UnmarshalJSON(data []byte) error {
545574
Hostname: relay.Hostname,
546575
}
547576
}
548-
p.PoolMetadata = tmp.PoolMetadata
577+
p.PoolMetadata = poolMetadata
549578

550579
// Handle margin field
551580
if len(tmp.Margin) > 0 {
@@ -585,27 +614,27 @@ func (p *PoolRegistrationCertificate) UnmarshalJSON(data []byte) error {
585614
}
586615

587616
// Convert operator key
588-
if tmp.Operator != "" {
589-
opBytes, err := hex.DecodeString(tmp.Operator)
617+
if operator != "" {
618+
opBytes, err := hex.DecodeString(operator)
590619
if err != nil {
591620
return fmt.Errorf("invalid operator key: %w", err)
592621
}
593622
p.Operator = PoolKeyHash(opBytes)
594623
}
595624

596625
// Convert VRF key hash
597-
if tmp.VrfKeyHash != "" {
598-
vrfBytes, err := hex.DecodeString(tmp.VrfKeyHash)
626+
if vrfKeyHash != "" {
627+
vrfBytes, err := hex.DecodeString(vrfKeyHash)
599628
if err != nil {
600629
return fmt.Errorf("invalid VRF key hash: %w", err)
601630
}
602631
p.VrfKeyHash = VrfKeyHash(NewBlake2b256(vrfBytes))
603632
}
604633

605634
// Convert pool owners
606-
if len(tmp.PoolOwners) > 0 {
607-
owners := make([]AddrKeyHash, len(tmp.PoolOwners))
608-
for i, owner := range tmp.PoolOwners {
635+
if len(poolOwners) > 0 {
636+
owners := make([]AddrKeyHash, len(poolOwners))
637+
for i, owner := range poolOwners {
609638
ownerBytes, err := hex.DecodeString(owner)
610639
if err != nil {
611640
return fmt.Errorf("invalid pool owner key: %w", err)

ledger/shelley/genesis_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,18 @@ func TestGenesisStaking(t *testing.T) {
320320
t.Errorf("Expected pool cost 340000000, got %d", pool.Cost)
321321
}
322322

323+
// The VRF key hash must be parsed from the genesis "vrf" field.
324+
// If it is dropped it reads as all-zeros, which breaks consensus
325+
// header VRF-key validation for this pool's blocks.
326+
expectedVrf := "eb53a17fbad9b7ea0bcf1e1ea89355305600d593b426dfc3084a924d8877d47e"
327+
if got := hex.EncodeToString(pool.VrfKeyHash[:]); got != expectedVrf {
328+
t.Errorf(
329+
"Expected pool VRF key hash %s, got %s",
330+
expectedVrf,
331+
got,
332+
)
333+
}
334+
323335
// Test delegators
324336
if len(delegators) != 1 {
325337
t.Errorf("Expected 1 delegator mapping, got %d", len(delegators))
@@ -369,6 +381,18 @@ func TestGenesisStaking(t *testing.T) {
369381
t.Errorf("Expected pool cost 340000000, got %d", pool.Cost)
370382
}
371383

384+
// The VRF key hash must be parsed from the genesis "vrf" field.
385+
// If it is dropped it reads as all-zeros, which breaks consensus
386+
// header VRF-key validation for this pool's blocks.
387+
expectedVrf := "eb53a17fbad9b7ea0bcf1e1ea89355305600d593b426dfc3084a924d8877d47e"
388+
if got := hex.EncodeToString(pool.VrfKeyHash[:]); got != expectedVrf {
389+
t.Errorf(
390+
"Expected pool VRF key hash %s, got %s",
391+
expectedVrf,
392+
got,
393+
)
394+
}
395+
372396
// Test delegators
373397
if len(delegators) != 1 {
374398
t.Errorf("Expected 1 delegator, got %d", len(delegators))

0 commit comments

Comments
 (0)