You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add /governance/committee/votes and /governance/committee/{cc_id}/votes. Both routes scan the committee vote histories in ProposalState. The scan keeps a bounded frontier of complete slot groups. It reads each block from the archive for the transaction hash, the vote anchor, the block height, and the intra-block order.
/governance/committee/{cc_id}/votes accepts a hot or cold CIP-129 credential. On-chain votes store the hot credential only. For a cold credential, Dolos finds the authorized hot credentials in committee_auths and committee_auth_archive.
Keep removed committee authorizations for API queries:
The Conway ledger EPOCH rule removes authorization histories of cold credentials that leave the committee.
Add GovState::committee_auth_archive at CBOR field index 8. The archive stores the authorization histories that the garbage collection removes. It is not ledger-effective state.
In CommitteeGc::apply, append each removed history to committee_auth_archive. CommitteeGc::undo truncates the appended entries.
Add GovState::committee_hot_credentials. It yields all hot credentials from active authorizations and the archive.
Read the archive in /governance/committee/{cc_id}/votes.
⚠️ [resync / replay] Stores synchronized with earlier versions ran the old garbage collection. That garbage collection erased past authorization histories. As a result, an existing store has an empty archive. To recover the erased histories and query past members by cold credential, you must replay or resync the store.
Testing
The synthetic test chain now carries a vote anchor.
Raise the gov snapshot schema revision from 1 to 2 and pin a revision-2 golden. The revision-1 golden stays for decode coverage.
The blockfrost-tests suite tests these endpoints in these files:
Auto reviews are disabled on base/target branches other than the default branch.
Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Advanced
Run ID: 1c34502f-490e-4c1c-bfa1-4fbcf31d574c
You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.
Use the checkbox below for a quick retry:
🔍 Trigger review
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
The EPOCH rule removes the authorization history of a cold credential
that leaves the committee. The Blockfrost committee vote endpoints must
still find the former hot credentials of that cold credential.
- Add `GovState::committee_auth_archive` at CBOR field index 8. The
archive holds only the histories that the GC removed. It is not
ledger-effective state.
- Move each removed history to the archive in `CommitteeGc::apply`.
The undo truncates the exact tail that its apply appended.
- Add `GovState::committee_hot_credentials`, which reads the archived
generations and the live generation.
- Read the archive in the cold-credential vote endpoint.
- Raise the `gov` snapshot schema revision to 2 and pin a revision-2
golden. The revision-1 golden stays for decode coverage.
An existing store has an empty archive. A replay or a resync is
necessary to recover the histories that an earlier GC deleted.
The endpoint tests used five rows and the default budget of 3000 items.
Thus, the tests did not operate the truncation path, the frontier
retry, or the scan budget.
- Add a fixture that puts four committee votes in four slots.
- Test that the scan budget rejects a history that is too long.
- Test that the row counter stops a scan when one slot holds more
votes than the budget of groups.
- Test that the retry increases a truncated frontier and returns the
full page in ascending and descending order.
- Test the same retry when a prune removes the blocks of a group.
- Test that a page at the group budget is short only when the rows do
not exist.
- Add `TestApp::new_with_scan_limit_and_setup`, which applies a scan
limit and a setup step.
The guard at the group budget cannot execute, because the row counter
stops the scan first. A comment records this. The guard stays, because
it makes sure that the retry loop ends.
Bumping gov to revision 2 makes the profile documentation inaccurate: crates/snapshot/PROFILE.md:152 still states that thirteen of fourteen live revisions are 1 and that epochs is the only/first bump. Update that compatibility-contract description to say that both epochs and gov are at revision 2.
A review asked whether a `CommitteeGc` delta from an older version can
reach `undo` and fail the archive assertion. It cannot, but the reason
was not in the code.
- Record why the assertion in `CommitteeGc::undo` is safe. The boundary
work unit keeps the default, empty `commit_wal`, and only
`RollWorkUnit` writes a `LogValue`. Thus, a rollback reads block
deltas only.
- Record why the `gov` schema revision is 2. `GovState` carries
`committee_auth_archive`. The text beside `SCHEMA_REVS` gave this
reason for `epochs` only.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟢 Approval recommended
The endpoint behavior, historical authorization model, rollback invariants, bounded scans, schema migration, and edge cases are comprehensively implemented and tested.
The reason will be displayed to describe this comment to others. Learn more.
Reviewed against blockfrost-backend-ryo (committee_votes.sql, committee_cc_id_votes.sql, validateCommitteeCredentialId) and the pinned blockfrost-tests fixtures. On f66b3b6e in an isolated worktree: clippy is clean for the files this PR touches, the 83 minibf governance tests, the 15 cardano committee tests and every snapshot suite pass.
What checks out: the listing order matches ryo's voting_procedure.id (tx order, then voter map order). Pallas orders ConstitutionalCommitteeScript before ConstitutionalCommitteeKey, which is the ledger's Credential order that db-sync inserts in, so the several-members-in-one-tx case matches. The cold-to-hot union over every recorded authorization matches ryo's committee_registration join. The 400 text is identical, unknown ids return 200 [], and the governance_type names match ryo's snake_case mapping. The gov revision bump follows the schema-rev process (rev 1 golden kept, rev 2 pinned). Uppercase bech32 ids are fine: CheckedHrpstring::hrp() is guaranteed lowercase.
Legend: 🔴 high = fix before merge (correctness, perf) · 🟡 medium = design or parity risk · 🟢 low = cleanup, tests, nits
The reason will be displayed to describe this comment to others. Learn more.
Severity: 🔴 high
For /governance/committee/votes this counter charges every committee vote in chain history against max_scan_items, whatever the page. Once the chain holds more than 3000 committee votes (the default), the endpoint returns 400 for ?count=1&page=1 and for every other request, permanently. Mainnet has 7 members voting on ~160 actions so far; preview and preprod test committees vote far more often. The only remedy is to raise max_scan_items, which also loosens the page cap on every other endpoint.
The counter does not protect anything the walk does not already pay. read_page for /governance/proposals decodes the same namespace without a budget. The frontier bounds memory to group_limit groups, and archive reads are bounded by group_limit <= to() <= max_scan_items. Rows inside one slot are bounded by the block size.
Suggest dropping the row budget and the two tests that pin it (rejects_history_over_scan_budget, counts_rows_inside_one_slot), and keeping the group_limit == budget guard as the loop terminator. Same point as #1368 (comment), but here the trigger is certain.
The reason will be displayed to describe this comment to others. Learn more.
Severity: 🟡 medium
Because unresolved rows are dropped before the page offset is counted, this loop settles every group from the frontier edge up to the page. The frontier holds to() groups, so ?count=100&page=30 decodes up to 3000 blocks per request on a full-archive node. If rows counted toward the offset before settling, only the groups that overlap [from, to) would need a block read (at most count + 1), and pruned nodes would return short pages instead of shifted ones.
Same trade-off as #1368 (comment); whichever way #1368 lands, the two endpoints should agree, and the choice deserves a sentence in the PR description.
The reason will be displayed to describe this comment to others. Learn more.
Severity: 🟡 medium
This is the inverse of bech32_committee_hot / bech32_committee_cold in crates/minibf/src/mapping.rs, but no test round-trips the pair. The route tests parse only a hot script id and a cold key id. The hot key URL in bad_request fails on page=0 before the parser runs, and no test parses a cold script id. A regression in the 0x2 branch for cc_hot or the 0x3 branch for cc_cold would pass the suite.
Please add a unit test that encodes all four (role, kind) combinations with the encoders and asserts the parse returns the same role and credential, plus a 28-byte and a 30-byte payload and a low nibble of 0x0/0x1 as rejections. Moving the parser next to bech32_committee would also let both sides share the HRP constants and the nibble values instead of repeating the literals here.
The reason will be displayed to describe this comment to others. Learn more.
Severity: 🟢 low
This test never truncates. group_limit = to().min(budget).max(1), and to() is count * page = 4 = budget, so the first frontier holds all four vote slots and the loop exits on the first pass. The comment says the first frontier holds count groups, which is not what the code does.
With a full archive the growth path is unreachable: to() groups always yield at least to() rows, so the page is full and the loop breaks. Growth only happens through pruning, which grows_past_a_pruned_group and returns_a_short_page_at_the_group_budget already cover. Either fix the comment and keep this as a plain paging check, or drop it.
The reason will be displayed to describe this comment to others. Learn more.
Severity: 🟢 low
settle_committee_casts and resolve_committee_vote_page repeat the block-matching and page-skipping logic of settle_casts and page_resolved_votes from the DRep path: decode the block, build a casts map with a VecDeque per target, pop_front in history order, sort by (tx position, index), drop rows without a cast, reverse for desc, skip from. The committee version is a superset (voter set, anchor, block height). Once #1368 settles, generalizing the DRep helpers over a voter filter and a row type would leave one copy to fix.
The reason will be displayed to describe this comment to others. Learn more.
Severity: 🟢 low
Every row is built here, with a clone of voter and vote, before push decides whether the frontier accepts it. On the aggregate endpoint most rows fall outside the frontier. A would_accept(slot) check on the frontier before building the row avoids the work. Separately, each retry of the loop in committee_vote_page rescans the whole namespace and re-decodes the blocks that the previous pass already settled; carrying settled groups across retries would make the pruned-node path cheaper.
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
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.
Resolves #1108.
Resolves #1110.
Merge this after:
/governance/dreps/{drep_id}/votes#1368Implementation
Add
/governance/committee/votesand/governance/committee/{cc_id}/votes. Both routes scan the committee vote histories inProposalState. The scan keeps a bounded frontier of complete slot groups. It reads each block from the archive for the transaction hash, the vote anchor, the block height, and the intra-block order./governance/committee/{cc_id}/votesaccepts a hot or cold CIP-129 credential. On-chain votes store the hot credential only. For a cold credential, Dolos finds the authorized hot credentials incommittee_authsandcommittee_auth_archive.Keep removed committee authorizations for API queries:
GovState::committee_auth_archiveat CBOR field index 8. The archive stores the authorization histories that the garbage collection removes. It is not ledger-effective state.CommitteeGc::apply, append each removed history tocommittee_auth_archive.CommitteeGc::undotruncates the appended entries.GovState::committee_hot_credentials. It yields all hot credentials from active authorizations and the archive./governance/committee/{cc_id}/votes.Testing
The synthetic test chain now carries a vote anchor.
Raise the
govsnapshot schema revision from 1 to 2 and pin a revision-2 golden. The revision-1 golden stays for decode coverage.The
blockfrost-testssuite tests these endpoints in these files: