-
Notifications
You must be signed in to change notification settings - Fork 32
feat(ledger): implement Shelley stake pool validation rules #2165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
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 84af1f1
fix(ledger): reject non-reward account headers
wolf31o2 cc43174
fix(ledger): invalidate pool reward account cache
wolf31o2 88da24d
fix(ledger): decode Shelley minPoolCost protocol parameter
wolf31o2 3d14a61
fix(ledger): match reward account header check to reference
wolf31o2 9f61fc3
chore(ledger): merge origin/main into Shelley pool rules
wolf31o2 d019b7d
test(ledger): close gaps in the POOL rule coverage
wolf31o2 cfbb636
fix(ledger): skip pool certificates of phase-2-invalid txs
wolf31o2 d32ccac
chore: merge main into pool validation
wolf31o2 faace0f
fix(ledger): enforce pool certificate predicates
wolf31o2 1bb8e4a
fix(ledger): preserve pool network metadata on cache clear
wolf31o2 a8081f0
test(ledger): cover mandatory pool epoch state
wolf31o2 015afc6
chore: merge origin/main into fix/2145-shelley-pool-rules
wolf31o2 991e502
fix(ledger): keep pool rule compatibility tests current
wolf31o2 d779412
fix(ledger): preserve Conway pool rule ordering
wolf31o2 856f20c
chore: merge origin/main into fix/2145-shelley-pool-rules
wolf31o2 01f6d89
fix(ledger): preserve optional epoch compatibility
wolf31o2 6992237
test(ledger): cover skipped retirement epoch bound
wolf31o2 46da2b1
docs(ledger): document EpochState epoch-zero exception
wolf31o2 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| // Copyright 2026 Blink Labs Software | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package common | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "testing" | ||
|
|
||
| "github.com/blinklabs-io/gouroboros/cbor" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| // TestPoolRegistrationRewardAccountNetworkId covers the network id recovered | ||
| // from a pool registration's reward account header byte, which the POOL rule's | ||
| // WrongNetworkPOOL predicate compares against the ledger's network. | ||
| func TestPoolRegistrationRewardAccountNetworkId(t *testing.T) { | ||
| credential := bytes.Repeat([]byte{0x07}, Blake2b224Size) | ||
| encode := func(t *testing.T, rewardAccount []byte) []byte { | ||
| t.Helper() | ||
| wire, err := cbor.Encode([]any{ | ||
| uint(CertificateTypePoolRegistration), | ||
| NewBlake2b224(bytes.Repeat([]byte{0x01}, Blake2b224Size)), | ||
| NewBlake2b256(bytes.Repeat([]byte{0x02}, Blake2b256Size)), | ||
| uint64(1_000_000), | ||
| uint64(340_000_000), | ||
| NewGenesisRat(0, 1), | ||
| rewardAccount, | ||
| []AddrKeyHash{}, | ||
| []PoolRelay{}, | ||
| nil, | ||
| }) | ||
| require.NoError(t, err) | ||
| return wire | ||
| } | ||
|
|
||
| for _, networkId := range []byte{0, 1} { | ||
| t.Run( | ||
| "header network "+string(rune('0'+networkId)), | ||
| func(t *testing.T) { | ||
| rewardAccount := append( | ||
| []byte{0xe0 | networkId}, | ||
| credential..., | ||
| ) | ||
| cert := &PoolRegistrationCertificate{} | ||
| require.NoError( | ||
| t, | ||
| cert.UnmarshalCBOR(encode(t, rewardAccount)), | ||
| ) | ||
| assert.Equal( | ||
| t, | ||
| AddrKeyHash(NewBlake2b224(credential)), | ||
| cert.RewardAccount, | ||
| ) | ||
| got, known := cert.RewardAccountNetworkId() | ||
| require.True(t, known) | ||
| assert.Equal(t, uint(networkId), got) | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| t.Run("script credential header keeps its network", func(t *testing.T) { | ||
| // 0xf0 is the script-hash reward-address header. Only the low | ||
| // nibble carries the network id. | ||
| rewardAccount := append([]byte{0xf1}, credential...) | ||
| cert := &PoolRegistrationCertificate{} | ||
| require.NoError(t, cert.UnmarshalCBOR(encode(t, rewardAccount))) | ||
| got, known := cert.RewardAccountNetworkId() | ||
| require.True(t, known) | ||
| assert.Equal(t, uint(1), got) | ||
| }) | ||
|
|
||
| t.Run("legacy 28-byte encoding has no network id", func(t *testing.T) { | ||
| cert := &PoolRegistrationCertificate{} | ||
| require.NoError(t, cert.UnmarshalCBOR(encode(t, credential))) | ||
| assert.Equal( | ||
| t, | ||
| AddrKeyHash(NewBlake2b224(credential)), | ||
| cert.RewardAccount, | ||
| ) | ||
| got, known := cert.RewardAccountNetworkId() | ||
| assert.False(t, known) | ||
| assert.Equal(t, uint(0), got) | ||
| }) | ||
|
|
||
| t.Run("constructed certificate has no network id", func(t *testing.T) { | ||
| cert := &PoolRegistrationCertificate{ | ||
| RewardAccount: NewBlake2b224(credential), | ||
| } | ||
| _, known := cert.RewardAccountNetworkId() | ||
| assert.False(t, known) | ||
| }) | ||
|
|
||
| t.Run("decoding preserves the wire bytes", func(t *testing.T) { | ||
| rewardAccount := append([]byte{0xe1}, credential...) | ||
| wire := encode(t, rewardAccount) | ||
| cert := &PoolRegistrationCertificate{} | ||
| require.NoError(t, cert.UnmarshalCBOR(wire)) | ||
| remarshaled, err := cert.MarshalCBOR() | ||
| require.NoError(t, err) | ||
| assert.Equal(t, wire, remarshaled) | ||
| }) | ||
| } | ||
|
|
||
| // TestPoolMetadataHashLengthIsFixed proves a pool registration whose metadata | ||
| // hash is not exactly 32 bytes fails to decode. The Shelley POOL rule's | ||
| // PoolMedataHashTooBig predicate is unreachable for that reason, so | ||
| // shelley.UtxoValidatePoolCertificates does not reimplement it. | ||
| func TestPoolMetadataHashLengthIsFixed(t *testing.T) { | ||
| encode := func(t *testing.T, metadata any) []byte { | ||
| t.Helper() | ||
| wire, err := cbor.Encode([]any{ | ||
| uint(CertificateTypePoolRegistration), | ||
| NewBlake2b224(bytes.Repeat([]byte{0x01}, Blake2b224Size)), | ||
| NewBlake2b256(bytes.Repeat([]byte{0x02}, Blake2b256Size)), | ||
| uint64(1_000_000), | ||
| uint64(340_000_000), | ||
| NewGenesisRat(0, 1), | ||
| append( | ||
| []byte{0xe1}, | ||
| bytes.Repeat([]byte{0x07}, Blake2b224Size)..., | ||
| ), | ||
| []AddrKeyHash{}, | ||
| []PoolRelay{}, | ||
| metadata, | ||
| }) | ||
| require.NoError(t, err) | ||
| return wire | ||
| } | ||
|
|
||
| t.Run("32-byte metadata hash decodes", func(t *testing.T) { | ||
| wire := encode(t, []any{ | ||
| "https://example.com/pool.json", | ||
| bytes.Repeat([]byte{0x05}, Blake2b256Size), | ||
| }) | ||
| cert := &PoolRegistrationCertificate{} | ||
| require.NoError(t, cert.UnmarshalCBOR(wire)) | ||
| require.NotNil(t, cert.PoolMetadata) | ||
| assert.Equal( | ||
| t, | ||
| PoolMetadataHash( | ||
| NewBlake2b256(bytes.Repeat([]byte{0x05}, Blake2b256Size)), | ||
| ), | ||
| cert.PoolMetadata.Hash, | ||
| ) | ||
| }) | ||
|
|
||
| wrongSizes := map[string]int{ | ||
| "one byte short": Blake2b256Size - 1, | ||
| "one byte long": Blake2b256Size + 1, | ||
| } | ||
| for name, size := range wrongSizes { | ||
| t.Run(name+" fails to decode", func(t *testing.T) { | ||
| wire := encode(t, []any{ | ||
| "https://example.com/pool.json", | ||
| bytes.Repeat([]byte{0x05}, size), | ||
| }) | ||
| cert := &PoolRegistrationCertificate{} | ||
| err := cert.UnmarshalCBOR(wire) | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "blake2b-256 hash") | ||
| }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.