Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
dca2c87
feat(ledger): implement Shelley stake pool validation rules
wolf31o2 Sep 2, 2026
84af1f1
fix(ledger): reject non-reward account headers
wolf31o2 Sep 2, 2026
cc43174
fix(ledger): invalidate pool reward account cache
wolf31o2 Sep 2, 2026
88da24d
fix(ledger): decode Shelley minPoolCost protocol parameter
wolf31o2 Sep 2, 2026
3d14a61
fix(ledger): match reward account header check to reference
wolf31o2 Sep 2, 2026
9f61fc3
chore(ledger): merge origin/main into Shelley pool rules
wolf31o2 Sep 2, 2026
d019b7d
test(ledger): close gaps in the POOL rule coverage
wolf31o2 Sep 2, 2026
cfbb636
fix(ledger): skip pool certificates of phase-2-invalid txs
wolf31o2 Sep 2, 2026
d32ccac
chore: merge main into pool validation
wolf31o2 Sep 2, 2026
faace0f
fix(ledger): enforce pool certificate predicates
wolf31o2 Sep 3, 2026
1bb8e4a
fix(ledger): preserve pool network metadata on cache clear
wolf31o2 Sep 3, 2026
a8081f0
test(ledger): cover mandatory pool epoch state
wolf31o2 Sep 3, 2026
015afc6
chore: merge origin/main into fix/2145-shelley-pool-rules
wolf31o2 Sep 3, 2026
991e502
fix(ledger): keep pool rule compatibility tests current
wolf31o2 Sep 3, 2026
d779412
fix(ledger): preserve Conway pool rule ordering
wolf31o2 Sep 3, 2026
856f20c
chore: merge origin/main into fix/2145-shelley-pool-rules
wolf31o2 Sep 3, 2026
01f6d89
fix(ledger): preserve optional epoch compatibility
wolf31o2 Sep 3, 2026
6992237
test(ledger): cover skipped retirement epoch bound
wolf31o2 Sep 3, 2026
46da2b1
docs(ledger): document EpochState epoch-zero exception
wolf31o2 Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ledger/allegra/pparams_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ func TestAllegraUtxorpc(t *testing.T) {
ProtocolMajor: 8,
ProtocolMinor: 0,
MinUtxoValue: 1000000,
MinPoolCost: 340000000,
}

expectedUtxorpc := &utxorpc.PParams{
Expand Down Expand Up @@ -118,6 +119,7 @@ func TestAllegraUtxorpc(t *testing.T) {
Major: 8,
Minor: 0,
},
MinPoolCost: common.ToUtxorpcBigInt(340000000),
}

result, err := inputParams.Utxorpc()
Expand Down
19 changes: 19 additions & 0 deletions ledger/allegra/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ var utxoValidationRuleDescriptors = []common.UtxoValidationRuleDescriptor{
Id: common.UtxoValidationRuleWithdrawals,
Validator: UtxoValidateWithdrawals,
},
{
Id: common.UtxoValidationRulePoolCertificates,
Validator: UtxoValidatePoolCertificates,
},
}

// UtxoValidationRuleDescriptors returns the authoritative ordered rule
Expand Down Expand Up @@ -341,6 +345,21 @@ func UtxoValidateWithdrawals(
return shelley.UtxoValidateWithdrawals(tx, slot, ls, pp)
}

// UtxoValidatePoolCertificates applies the Shelley POOL rule, which this era
// inherits unchanged.
//
// Reference: eras/allegra/impl/src/Cardano/Ledger/Allegra/Rules/Pool.hs
// declares only the EraRuleFailure and EraRuleEvent instances and reuses
// Shelley.poolTransition.
func UtxoValidatePoolCertificates(
tx common.Transaction,
slot uint64,
ls common.LedgerState,
pp common.ProtocolParameters,
) error {
return shelley.UtxoValidatePoolCertificates(tx, slot, ls, pp)
}

// UtxoValidateMIRGenesisQuorum ensures a move instantaneous rewards
// certificate is authorized by a quorum of the current genesis delegates
func UtxoValidateMIRGenesisQuorum(
Expand Down
15 changes: 15 additions & 0 deletions ledger/alonzo/pparams.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,18 @@ func UpgradePParams(
MinPoolCost: prevPParams.MinPoolCost,
}
}

// ProtocolMajorVersion returns the active major protocol version.
func (p *AlonzoProtocolParameters) ProtocolMajorVersion() uint {
return p.ProtocolMajor
}

// MinPoolCostValue returns the minPoolCost protocol parameter.
func (p *AlonzoProtocolParameters) MinPoolCostValue() uint64 {
return p.MinPoolCost
}

// PoolRetirementMaxEpoch returns the eMax protocol parameter.
func (p *AlonzoProtocolParameters) PoolRetirementMaxEpoch() uint64 {
return uint64(p.MaxEpoch)
}
23 changes: 22 additions & 1 deletion ledger/alonzo/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ var utxoValidationRuleDescriptors = []common.UtxoValidationRuleDescriptor{
Id: common.UtxoValidationRuleWithdrawals,
Validator: UtxoValidateWithdrawals,
},
{
Id: common.UtxoValidationRulePoolCertificates,
Validator: UtxoValidatePoolCertificates,
},
}

// UtxoValidationRuleDescriptors returns the authoritative ordered rule
Expand Down Expand Up @@ -175,7 +179,9 @@ var UtxoValidationRules = common.ComposeUtxoValidationRules(
UtxoValidateExUnitsTooBigUtxo, UtxoValidateNativeScripts,
UtxoValidateExtraneousRedeemers, UtxoValidatePlutusScripts,
),
common.Phase2ValidUtxoValidationRules(UtxoValidateDelegation, UtxoValidateWithdrawals),
common.Phase2ValidUtxoValidationRules(
UtxoValidateDelegation, UtxoValidateWithdrawals, UtxoValidatePoolCertificates,
),
)

// UtxoValidateOutputTooBigUtxo ensures that transaction output values are not too large
Expand Down Expand Up @@ -1063,6 +1069,21 @@ func UtxoValidateScriptDataHash(
return nil
}

// UtxoValidatePoolCertificates applies the Shelley POOL rule, which this era
// inherits unchanged.
//
// Reference: eras/alonzo/impl/src/Cardano/Ledger/Alonzo/Rules/Pool.hs declares
// only the EraRuleFailure and EraRuleEvent instances and reuses
// Shelley.poolTransition.
func UtxoValidatePoolCertificates(
tx common.Transaction,
slot uint64,
ls common.LedgerState,
pp common.ProtocolParameters,
) error {
return shelley.UtxoValidatePoolCertificates(tx, slot, ls, pp)
}

// UtxoValidateMIRGenesisQuorum ensures a move instantaneous rewards
// certificate is authorized by a quorum of the current genesis delegates
func UtxoValidateMIRGenesisQuorum(
Expand Down
15 changes: 15 additions & 0 deletions ledger/babbage/pparams.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,18 @@ func UpgradePParams(
}
return ret
}

// ProtocolMajorVersion returns the active major protocol version.
func (p *BabbageProtocolParameters) ProtocolMajorVersion() uint {
return p.ProtocolMajor
}

// MinPoolCostValue returns the minPoolCost protocol parameter.
func (p *BabbageProtocolParameters) MinPoolCostValue() uint64 {
return p.MinPoolCost
}

// PoolRetirementMaxEpoch returns the eMax protocol parameter.
func (p *BabbageProtocolParameters) PoolRetirementMaxEpoch() uint64 {
return uint64(p.MaxEpoch)
}
23 changes: 22 additions & 1 deletion ledger/babbage/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ var utxoValidationRuleDescriptors = []common.UtxoValidationRuleDescriptor{
Id: common.UtxoValidationRuleWithdrawals,
Validator: UtxoValidateWithdrawals,
},
{
Id: common.UtxoValidationRulePoolCertificates,
Validator: UtxoValidatePoolCertificates,
},
}

// UtxoValidationRuleDescriptors returns the authoritative ordered rule
Expand Down Expand Up @@ -204,7 +208,9 @@ var UtxoValidationRules = common.ComposeUtxoValidationRules(
UtxoValidateExtraneousRedeemers, UtxoValidateMalformedReferenceScripts,
UtxoValidatePlutusScripts,
),
common.Phase2ValidUtxoValidationRules(UtxoValidateDelegation, UtxoValidateWithdrawals),
common.Phase2ValidUtxoValidationRules(
UtxoValidateDelegation, UtxoValidateWithdrawals, UtxoValidatePoolCertificates,
),
)

func UtxoValidateOutsideValidityIntervalUtxo(
Expand Down Expand Up @@ -1435,6 +1441,21 @@ func UtxoValidateMalformedReferenceScripts(
return common.ValidatePlutusScriptsWellFormed(tx, params.ProtocolMajor)
}

// UtxoValidatePoolCertificates applies the Shelley POOL rule, which this era
// inherits unchanged.
//
// Reference: eras/babbage/impl/src/Cardano/Ledger/Babbage/Rules/Pool.hs
// declares only the EraRuleFailure and EraRuleEvent instances and reuses
// Shelley.poolTransition.
func UtxoValidatePoolCertificates(
tx common.Transaction,
slot uint64,
ls common.LedgerState,
pp common.ProtocolParameters,
) error {
return shelley.UtxoValidatePoolCertificates(tx, slot, ls, pp)
}

// UtxoValidateMIRGenesisQuorum ensures a move instantaneous rewards
// certificate is authorized by a quorum of the current genesis delegates
func UtxoValidateMIRGenesisQuorum(
Expand Down
37 changes: 36 additions & 1 deletion ledger/common/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,35 @@ type PoolRegistrationCertificate struct {
PoolOwners []AddrKeyHash `json:"poolOwners"`
Relays []PoolRelay `json:"relays"`
PoolMetadata *PoolMetadata `json:"poolMetadata,omitempty"`

// rewardAccountNetworkId holds the network id from the address header
// byte of the wire reward_account, which RewardAccount itself does not
// retain. rewardAccountNetworkIdKnown is false when the certificate was
// not decoded from a CBOR reward account carrying a header byte (a
// programmatically constructed certificate, one built from JSON, or the
// legacy 28-byte encoding).
rewardAccountNetworkId uint
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
rewardAccountNetworkIdKnown bool
}

// RewardAccountNetworkId returns the network id encoded in the header byte of
// the certificate's reward account. The second return value is false when the
// certificate did not come from a wire reward_account carrying that header, in
// which case no network id is recoverable and callers must not treat the zero
// value as testnet.
func (c *PoolRegistrationCertificate) RewardAccountNetworkId() (uint, bool) {
return c.rewardAccountNetworkId, c.rewardAccountNetworkIdKnown
}

// SetCbor invalidates the decoded reward-account network only when replacing
// the cached bytes. Clearing the cache before mutating fields must preserve
// that consensus-relevant metadata.
func (c *PoolRegistrationCertificate) SetCbor(cborData []byte) {
c.DecodeStoreCbor.SetCbor(cborData)
if cborData != nil {
c.rewardAccountNetworkId = 0
c.rewardAccountNetworkIdKnown = false
}
}

// ErrPoolMarginOutsideUnitInterval identifies a stake-pool margin outside the
Expand Down Expand Up @@ -1076,6 +1105,8 @@ func (c *PoolRegistrationCertificate) UnmarshalCBOR(cborData []byte) error {
c.Cost = tmp.Cost
c.Margin = tmp.Margin
c.RewardAccount = tmp.RewardAccount.credential
c.rewardAccountNetworkId = tmp.RewardAccount.networkId
c.rewardAccountNetworkIdKnown = tmp.RewardAccount.networkIdKnown
c.PoolOwners = tmp.PoolOwners
c.Relays = tmp.Relays
c.PoolMetadata = tmp.PoolMetadata
Expand All @@ -1097,6 +1128,8 @@ func (c *PoolRegistrationCertificate) UnmarshalCBOR(cborData []byte) error {
c.Cost = tmp.Cost
c.Margin = tmp.Margin
c.RewardAccount = tmp.RewardAccount.credential
c.rewardAccountNetworkId = tmp.RewardAccount.networkId
c.rewardAccountNetworkIdKnown = tmp.RewardAccount.networkIdKnown
c.PoolOwners = tmp.PoolOwners
c.Relays = tmp.Relays
c.PoolMetadata = tmp.PoolMetadata
Expand All @@ -1106,7 +1139,9 @@ func (c *PoolRegistrationCertificate) UnmarshalCBOR(cborData []byte) error {
len(fields),
)
}
c.SetCbor(cborData)
// Preserve the header metadata decoded above; the public SetCbor method
// intentionally clears that metadata when callers replace the cache.
c.DecodeStoreCbor.SetCbor(cborData)
return nil
}

Expand Down
46 changes: 46 additions & 0 deletions ledger/common/certs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,52 @@ func TestPoolRegistrationCertificateRewardAccountDecode(t *testing.T) {
credential...,
),
},
{
name: "non-reward address",
rewardAccount: append(
[]byte{0x01},
credential...,
),
wantErr: "invalid reward account address header",
},
{
// 0xf1 is the script-credential reward-address header on
// mainnet, the last of the four headers
// headerIsAccountAddress admits.
name: "script reward address",
rewardAccount: append(
[]byte{0xf1},
credential...,
),
},
// headerIsAccountAddress in Cardano.Ledger.Address requires
// header .&. 0b11101110 == 0b11100000, so bits 3-1 must be clear
// and only 0xe0, 0xe1, 0xf0 and 0xf1 are valid. Each header below
// has the reward-address high nibble but a reserved bit set.
{
name: "reserved bit 1 set",
rewardAccount: append(
[]byte{0xe2},
credential...,
),
wantErr: "invalid reward account address header",
},
{
name: "reserved bit 2 set",
rewardAccount: append(
[]byte{0xe5},
credential...,
),
wantErr: "invalid reward account address header",
},
{
name: "reserved bit 3 set",
rewardAccount: append(
[]byte{0xe8},
credential...,
),
wantErr: "invalid reward account address header",
},
{
name: "long",
rewardAccount: bytes.Repeat([]byte{0x03}, Blake2b224Size+2),
Expand Down
Loading