Skip to content

Commit ac6eddd

Browse files
authored
feat(ledger): stake pool validation (#1315)
Signed-off-by: Chris Gianelloni <[email protected]>
1 parent adb2f5b commit ac6eddd

File tree

4 files changed

+236
-3
lines changed

4 files changed

+236
-3
lines changed

ledger/common/rules_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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 common
16+
17+
import (
18+
"errors"
19+
"testing"
20+
"time"
21+
22+
"github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano"
23+
)
24+
25+
func TestVerifyTransaction(t *testing.T) {
26+
// Mock transaction - we don't need a real one for this test
27+
var tx Transaction
28+
29+
slot := uint64(1000)
30+
ledgerState := &mockLedgerStateRules{}
31+
protocolParams := &mockProtocolParamsRules{}
32+
33+
t.Run("all_rules_pass", func(t *testing.T) {
34+
rules := []UtxoValidationRuleFunc{
35+
func(Transaction, uint64, LedgerState, ProtocolParameters) error { return nil },
36+
func(Transaction, uint64, LedgerState, ProtocolParameters) error { return nil },
37+
func(Transaction, uint64, LedgerState, ProtocolParameters) error { return nil },
38+
}
39+
40+
err := VerifyTransaction(tx, slot, ledgerState, protocolParams, rules)
41+
if err != nil {
42+
t.Errorf("expected no error, got %v", err)
43+
}
44+
})
45+
46+
t.Run("first_rule_fails", func(t *testing.T) {
47+
expectedErr := errors.New("first rule failed")
48+
rules := []UtxoValidationRuleFunc{
49+
func(Transaction, uint64, LedgerState, ProtocolParameters) error { return expectedErr },
50+
func(Transaction, uint64, LedgerState, ProtocolParameters) error { return nil },
51+
}
52+
53+
err := VerifyTransaction(tx, slot, ledgerState, protocolParams, rules)
54+
if err != expectedErr {
55+
t.Errorf("expected error %v, got %v", expectedErr, err)
56+
}
57+
})
58+
59+
t.Run("middle_rule_fails", func(t *testing.T) {
60+
expectedErr := errors.New("middle rule failed")
61+
rules := []UtxoValidationRuleFunc{
62+
func(Transaction, uint64, LedgerState, ProtocolParameters) error { return nil },
63+
func(Transaction, uint64, LedgerState, ProtocolParameters) error { return expectedErr },
64+
func(Transaction, uint64, LedgerState, ProtocolParameters) error { return nil },
65+
}
66+
67+
err := VerifyTransaction(tx, slot, ledgerState, protocolParams, rules)
68+
if err != expectedErr {
69+
t.Errorf("expected error %v, got %v", expectedErr, err)
70+
}
71+
})
72+
73+
t.Run("last_rule_fails", func(t *testing.T) {
74+
expectedErr := errors.New("last rule failed")
75+
rules := []UtxoValidationRuleFunc{
76+
func(Transaction, uint64, LedgerState, ProtocolParameters) error { return nil },
77+
func(Transaction, uint64, LedgerState, ProtocolParameters) error { return expectedErr },
78+
}
79+
80+
err := VerifyTransaction(tx, slot, ledgerState, protocolParams, rules)
81+
if err != expectedErr {
82+
t.Errorf("expected error %v, got %v", expectedErr, err)
83+
}
84+
})
85+
86+
t.Run("empty_rules", func(t *testing.T) {
87+
rules := []UtxoValidationRuleFunc{}
88+
89+
err := VerifyTransaction(tx, slot, ledgerState, protocolParams, rules)
90+
if err != nil {
91+
t.Errorf("expected no error with empty rules, got %v", err)
92+
}
93+
})
94+
}
95+
96+
// Mock types for testing
97+
type mockLedgerStateRules struct{}
98+
99+
func (m *mockLedgerStateRules) UtxoById(
100+
input TransactionInput,
101+
) (Utxo, error) {
102+
return Utxo{}, nil
103+
}
104+
105+
func (m *mockLedgerStateRules) StakeRegistration(
106+
key []byte,
107+
) ([]StakeRegistrationCertificate, error) {
108+
return nil, nil
109+
}
110+
111+
func (m *mockLedgerStateRules) SlotToTime(
112+
slot uint64,
113+
) (time.Time, error) {
114+
return time.Time{}, nil
115+
}
116+
117+
func (m *mockLedgerStateRules) TimeToSlot(
118+
t time.Time,
119+
) (uint64, error) {
120+
return 0, nil
121+
}
122+
123+
func (m *mockLedgerStateRules) PoolCurrentState(
124+
poolKeyHash PoolKeyHash,
125+
) (*PoolRegistrationCertificate, *uint64, error) {
126+
return nil, nil, nil
127+
}
128+
129+
func (m *mockLedgerStateRules) CalculateRewards(
130+
pots AdaPots,
131+
snapshot RewardSnapshot,
132+
params RewardParameters,
133+
) (*RewardCalculationResult, error) {
134+
return nil, errors.New("not implemented")
135+
}
136+
137+
func (m *mockLedgerStateRules) GetAdaPots() AdaPots { return AdaPots{} }
138+
func (m *mockLedgerStateRules) UpdateAdaPots(pots AdaPots) error { return nil }
139+
140+
func (m *mockLedgerStateRules) GetRewardSnapshot(
141+
epoch uint64,
142+
) (RewardSnapshot, error) {
143+
return RewardSnapshot{}, nil
144+
}
145+
func (m *mockLedgerStateRules) NetworkId() uint { return 0 }
146+
147+
type mockProtocolParamsRules struct{}
148+
149+
func (m *mockProtocolParamsRules) Utxorpc() (*cardano.PParams, error) { return nil, nil }

ledger/common/verify_config.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ import (
2626
// Default values favor safety; tests or specific flows can opt out.
2727
type VerifyConfig struct {
2828
// SkipBodyHashValidation disables body hash verification in VerifyBlock().
29+
// When false (default), full block CBOR must be available for validation.
2930
// Useful for scenarios where full block CBOR is unavailable.
3031
SkipBodyHashValidation bool
3132
// SkipTransactionValidation disables transaction validation in VerifyBlock().
3233
// When false (default), LedgerState and ProtocolParameters must be set.
3334
SkipTransactionValidation bool
35+
// SkipStakePoolValidation disables stake pool registration validation in VerifyBlock().
36+
// When false (default), LedgerState must be set.
37+
SkipStakePoolValidation bool
3438
// LedgerState provides the current ledger state for transaction validation.
35-
// Required if SkipTransactionValidation is false.
39+
// Required if SkipTransactionValidation or SkipStakePoolValidation is false.
3640
LedgerState LedgerState
3741
// ProtocolParameters provides the current protocol parameters for transaction validation.
3842
// Required if SkipTransactionValidation is false.

ledger/verify_block.go

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,9 @@ func VerifyBlock(
337337
)
338338
}
339339
if config.LedgerState == nil || config.ProtocolParameters == nil {
340-
return false, "", 0, 0, errors.New("VerifyBlock: missing required config field: LedgerState and ProtocolParameters must be set")
340+
return false, "", 0, 0, errors.New(
341+
"VerifyBlock: missing required config field: LedgerState and ProtocolParameters must be set",
342+
)
341343
}
342344
for _, tx := range block.Transactions() {
343345
if err := common.VerifyTransaction(tx, slot, config.LedgerState, config.ProtocolParameters, validationRules); err != nil {
@@ -349,6 +351,83 @@ func VerifyBlock(
349351
}
350352
}
351353

354+
// Verify stake pool registration (can be skipped via config)
355+
// Requires LedgerState in config if enabled.
356+
if block.Era() != byron.EraByron && !config.SkipStakePoolValidation {
357+
if config.LedgerState == nil {
358+
return false, "", 0, 0, errors.New(
359+
"VerifyBlock: missing required config field: LedgerState must be set for stake pool validation",
360+
)
361+
}
362+
363+
// Extract pool key hash from issuer vkey
364+
var issuerVkey []byte
365+
switch h := block.Header().(type) {
366+
case *shelley.ShelleyBlockHeader:
367+
issuerVkey = h.Body.IssuerVkey[:]
368+
case *allegra.AllegraBlockHeader:
369+
issuerVkey = h.Body.IssuerVkey[:]
370+
case *mary.MaryBlockHeader:
371+
issuerVkey = h.Body.IssuerVkey[:]
372+
case *alonzo.AlonzoBlockHeader:
373+
issuerVkey = h.Body.IssuerVkey[:]
374+
case *babbage.BabbageBlockHeader:
375+
issuerVkey = h.Body.IssuerVkey[:]
376+
case *conway.ConwayBlockHeader:
377+
issuerVkey = h.Body.IssuerVkey[:]
378+
default:
379+
return false, "", 0, 0, fmt.Errorf(
380+
"VerifyBlock: unsupported block type for stake pool validation %T",
381+
block.Header(),
382+
)
383+
}
384+
385+
poolKeyHash := common.Blake2b224Hash(issuerVkey)
386+
387+
// Check if pool is registered
388+
poolCert, _, err := config.LedgerState.PoolCurrentState(poolKeyHash)
389+
if err != nil {
390+
return false, "", 0, 0, fmt.Errorf(
391+
"VerifyBlock: failed to query pool state: %w",
392+
err,
393+
)
394+
}
395+
if poolCert == nil {
396+
return false, "", 0, 0, fmt.Errorf(
397+
"VerifyBlock: pool %s is not registered",
398+
poolKeyHash.String(),
399+
)
400+
}
401+
402+
// Check if VRF key matches registered pool's VRF key
403+
var blockVrfKey []byte
404+
switch h := block.Header().(type) {
405+
case *shelley.ShelleyBlockHeader:
406+
blockVrfKey = h.Body.VrfKey
407+
case *allegra.AllegraBlockHeader:
408+
blockVrfKey = h.Body.VrfKey
409+
case *mary.MaryBlockHeader:
410+
blockVrfKey = h.Body.VrfKey
411+
case *alonzo.AlonzoBlockHeader:
412+
blockVrfKey = h.Body.VrfKey
413+
case *babbage.BabbageBlockHeader:
414+
blockVrfKey = h.Body.VrfKey
415+
case *conway.ConwayBlockHeader:
416+
blockVrfKey = h.Body.VrfKey
417+
}
418+
419+
registeredVrfKeyHash := poolCert.VrfKeyHash
420+
expectedVrfKeyHash := common.Blake2b256Hash(blockVrfKey)
421+
if registeredVrfKeyHash != expectedVrfKeyHash {
422+
return false, "", 0, 0, fmt.Errorf(
423+
"VerifyBlock: VRF key mismatch for pool %s: expected %s, got %s",
424+
poolKeyHash.String(),
425+
registeredVrfKeyHash.String(),
426+
expectedVrfKeyHash.String(),
427+
)
428+
}
429+
}
430+
352431
isValid = isBodyValid && vrfValid && kesValid
353432
slotNo := slot
354433
return isValid, vrfHex, blockNo, slotNo, nil

ledger/verify_block_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ var eraNameMap = map[uint]string{
2626
BlockTypeConway: conway.EraNameConway,
2727
}
2828

29-
// testSkipAllValidationConfig returns a VerifyConfig that skips both body hash and transaction validation.
29+
// testSkipAllValidationConfig returns a VerifyConfig that skips body hash, stake pool, and transaction validation.
3030
// Useful for tests that don't provide full CBOR or ledger state.
3131
func testSkipAllValidationConfig() common.VerifyConfig {
3232
return common.VerifyConfig{
3333
SkipBodyHashValidation: true,
3434
SkipTransactionValidation: true,
35+
SkipStakePoolValidation: true,
3536
}
3637
}
3738

0 commit comments

Comments
 (0)