Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Other guiding principles:
- **amaru-ledger**: validate voters in a transaction actually exist in ledger state. ([#1138][], [#923][])
- **amaru-ledger**: validate the governance actions a transaction votes on actually exist, counting proposals submitted earlier in the same block. ([#1139][], [#924][])
- **amaru-ledger**: reject votes cast on governance actions that have expired. A proposal's expiry is now stamped once, when it is submitted, and carried through the volatile state, which will resolve some potential bugs. ([#1143][], [#926][])
- **amaru-ledger**: from protocol version 11 on, reject votes cast by constitutional committee members the *elected* committee does not name, even when they hold an authorized hot credential. ([#1157][], [#922][])

## [v10.11.20260806](https://github.com/pragma-org/amaru/releases/tag/v10.11.20260807)

Expand Down Expand Up @@ -272,6 +273,7 @@ Other guiding principles:
[#909]: https://github.com/pragma-org/amaru/issues/909
[#912]: https://github.com/pragma-org/amaru/issues/912
[#915]: https://github.com/pragma-org/amaru/issues/915
[#922]: https://github.com/pragma-org/amaru/issues/922
[#923]: https://github.com/pragma-org/amaru/issues/923
[#924]: https://github.com/pragma-org/amaru/issues/924
[#926]: https://github.com/pragma-org/amaru/issues/926
Expand Down Expand Up @@ -330,3 +332,4 @@ Other guiding principles:
[#1138]: https://github.com/pragma-org/amaru/pull/1138
[#1139]: https://github.com/pragma-org/amaru/pull/1139
[#1143]: https://github.com/pragma-org/amaru/pull/1143
[#1157]: https://github.com/pragma-org/amaru/pull/1157
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ pub(super) enum Predicate {
StakePoolRetirementWrongEpochPOOL,
StakePoolNotRegisteredOnKeyPOOL,
StakePoolCostTooLowPOOL,
UnelectedCommitteeVoters,
ValueNotConservedUTxO,
VotersDoNotExist,
VotingOnExpiredGovAction,
Expand Down Expand Up @@ -365,6 +366,9 @@ impl From<PhaseOneError> for Predicate {
PhaseOneError::Proposals(InvalidProposals::ProposalReturnAccountDoesNotExist(_)) => {
Predicate::ProposalReturnAccountDoesNotExist
}
PhaseOneError::VotingProcedures(InvalidVotingProcedures::UnelectedCommitteeVoter(_)) => {
Predicate::UnelectedCommitteeVoters
}
PhaseOneError::VotingProcedures(InvalidVotingProcedures::VotersDoNotExist(_)) => {
Predicate::VotersDoNotExist
}
Expand Down
8 changes: 7 additions & 1 deletion crates/amaru-ledger/src/rules/transaction/phase_one/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,13 @@ where
})?;

debug_span!(ledger::rules::phase_one::VOTES).in_scope(|| {
voting_procedures::execute(context, era_history, pointer, mem::take(&mut transaction_body.votes))
voting_procedures::execute(
context,
protocol_parameters.protocol_version,
era_history,
pointer,
mem::take(&mut transaction_body.votes),
)
})?;
} else {
debug_span!(ledger::rules::phase_one::CERTIFICATES).in_scope(|| {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@
use std::collections::{BTreeMap, BTreeSet};

use amaru_kernel::{
EraHistory, HasOwnership, MemoizedDatum, NonEmptyKeyValuePairs, ProposalId, RedeemerTag, RequiredScript,
StakeCredential, TransactionPointer, Voter, VotingProcedure,
EraHistory, HasOwnership, MemoizedDatum, NonEmptyKeyValuePairs, ProposalId, ProtocolVersion, RedeemerTag,
RequiredScript, StakeCredential, TransactionPointer, Voter, VotingProcedure,
};
use thiserror::Error;

use crate::context::{CommitteeSlice, DRepsSlice, PoolsSlice, ProposalsSlice, WitnessSlice};

#[derive(Debug, Error)]
pub enum InvalidVotingProcedures {
#[error("vote cast by committee member who is not yet elected: {0:?}")]
UnelectedCommitteeVoter(Voter),

#[error("voters do not exist: {0:?}")]
VotersDoNotExist(BTreeSet<Voter>),

Expand All @@ -39,6 +42,7 @@ pub enum InvalidVotingProcedures {

pub(crate) fn execute<C>(
context: &mut C,
protocol_version: ProtocolVersion,
era_history: &EraHistory,
pointer: TransactionPointer,
voting_procedures: Option<NonEmptyKeyValuePairs<Voter, NonEmptyKeyValuePairs<ProposalId, VotingProcedure>>>,
Expand All @@ -57,6 +61,10 @@ where
let mut unknown_proposals = BTreeSet::new();

for (voter, votes) in voting_procedures.iter() {
if protocol_version.0 > 10 && is_unelected_committee_voter(context, voter) {
return Err(InvalidVotingProcedures::UnelectedCommitteeVoter(voter.clone()));
}

if !exists(context, voter) {
unknown_voters.insert(voter.clone());
}
Expand Down Expand Up @@ -106,6 +114,24 @@ where
Ok(())
}

/// Election statushere is membership, not an unexpired term: a member whose term has run out is still
/// named by the committee, and their vote is discounted when the action is ratified instead.
///
/// Voters that are not committee members are never rejected by this check.
fn is_unelected_committee_voter<C>(context: &C, voter: &Voter) -> bool
where
C: CommitteeSlice,
{
match voter {
Voter::ConstitutionalCommitteeKey(_) | Voter::ConstitutionalCommitteeScript(_) => {
!CommitteeSlice::lookup_by_hot_credential(context, &voter.owner())
.iter()
.any(|member| member.valid_until.is_some())
}
Voter::DRepKey(_) | Voter::DRepScript(_) | Voter::StakePoolKey(_) => false,
}
}

/// Whether the entity a vote is cast by is known at this point in the block.
fn exists<C>(context: &C, voter: &Voter) -> bool
where
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"title": "vote cast by an unelected committee member with an authorized hot key at protocol version 11",
"description": "A member holding no term but an authorized hot key.",
"network": "preprod",
"eraHistory": {
"$ref": "common/eraHistory/preprod-conway.json"
},
"protocolParameters": {
"$ref": "common/protocolParameters/preprod-conway-v10.json",
"$override": {
"version": {
"major": 11,
"minor": 0
}
}
},
"initialState": {
"utxo": [
{
"input": "825820e4ab7bd9de7060f2f81fc907132ce2942081ac10b2b9454d53642eb304a64a3800",
"output": "a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a004c4b40"
}
],
"pools": [],
"accounts": [],
"dreps": [],
"committee": [
{
"coldCredential": "8200581cecc3093b061a9ed426cd9956161fc48d4ed1ac58dc4aaed7e1ce1830",
"hotCredential": "8200581c93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8",
"validUntil": null
}
],
"proposals": [
{
"id": "8258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b00",
"validUntil": 100
}
],
"governanceActivity": {
"consecutiveDormantEpochs": 0
},
"pots": {
"treasury": 0,
"reserves": 0
}
},
"point": {
"slot": 0,
"transactionIndex": 0
},
"transaction": "84a500d9010281825820e4ab7bd9de7060f2f81fc907132ce2942081ac10b2b9454d53642eb304a64a38000181a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a00493e00021a00030d400f0013a18200581c93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8a18258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b008201f6a100d90102818258202152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db125840f46d6100d906cb1424b6f9234fd187090bddf85d0453f8f09700cbcdf7491556c325dac760dc35efd1dc234e532e357d413aa35a99c1ba2310ed73caa9306008f5f6",
"expected": {
"predicate": "UnelectedCommitteeVoters"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"title": "vote cast by an unelected committee member with an authorized hot script credential",
"description": "The hot script credential case of an authorization held by a member with no term.",
"network": "preprod",
"eraHistory": {
"$ref": "common/eraHistory/preprod-conway.json"
},
"protocolParameters": {
"$ref": "common/protocolParameters/preprod-conway-v10.json",
"$override": {
"version": {
"major": 11,
"minor": 0
}
}
},
"initialState": {
"utxo": [
{
"input": "825820b9488a2acb2a4de446769c8e4b2aebf0045e79ee0f7ca36473f94113c34918bf00",
"output": "a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a004c4b40"
}
],
"pools": [],
"accounts": [],
"dreps": [],
"committee": [
{
"coldCredential": "8200581cecc3093b061a9ed426cd9956161fc48d4ed1ac58dc4aaed7e1ce1830",
"hotCredential": "8201581cd441227553a0f1a965fee7d60a0f724b368dd1bddbc208730fccebcf",
"validUntil": null
}
],
"proposals": [
{
"id": "8258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b00",
"validUntil": 100
}
],
"governanceActivity": {
"consecutiveDormantEpochs": 0
},
"pots": {
"treasury": 0,
"reserves": 0
}
},
"point": {
"slot": 0,
"transactionIndex": 0
},
"transaction": "84a500d9010281825820b9488a2acb2a4de446769c8e4b2aebf0045e79ee0f7ca36473f94113c34918bf000181a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a00493e00021a00030d400f0013a18201581cd441227553a0f1a965fee7d60a0f724b368dd1bddbc208730fccebcfa18258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b008201f6a200d90102818258202152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db125840920b2ee06e8fdbebd15ddfa5c34d00b8197bc3b0ecd2f99d0dff19d9b8093e4de816b5f007239fe7ef4593a9414771bf0b26160cb255fd8e0025917f4b49bc060181820180f5f6",
"expected": {
"predicate": "UnelectedCommitteeVoters"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"title": "vote cast by an unelected member's hot key while an elected member holds another",
"description": "Two authorizations, one by an elected member and one by a member with no term, with the vote naming the unelected member's hot key.",
"network": "preprod",
"eraHistory": {
"$ref": "common/eraHistory/preprod-conway.json"
},
"protocolParameters": {
"$ref": "common/protocolParameters/preprod-conway-v10.json",
"$override": {
"version": {
"major": 11,
"minor": 0
}
}
},
"initialState": {
"utxo": [
{
"input": "8258205a69bb3f301c252d55c3cde9eb24c01988937c00e2dec75ea0f64625fb408a5700",
"output": "a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a004c4b40"
}
],
"pools": [],
"accounts": [],
"dreps": [],
"committee": [
{
"coldCredential": "8200581c2e06285720a42375de8c91a3f9f27d89516f93a2f60ad638ef30eea6",
"hotCredential": "8200581cba40f56c8091f0646c5c55e15d5b19d555d4a59aaf1b6d09b5ce19c8",
"validUntil": 200
},
{
"coldCredential": "8200581cecc3093b061a9ed426cd9956161fc48d4ed1ac58dc4aaed7e1ce1830",
"hotCredential": "8200581c93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8",
"validUntil": null
}
],
"proposals": [
{
"id": "8258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b00",
"validUntil": 100
}
],
"governanceActivity": {
"consecutiveDormantEpochs": 0
},
"pots": {
"treasury": 0,
"reserves": 0
}
},
"point": {
"slot": 0,
"transactionIndex": 0
},
"transaction": "84a500d90102818258205a69bb3f301c252d55c3cde9eb24c01988937c00e2dec75ea0f64625fb408a57000181a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a00493e00021a00030d400f0013a18200581c93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8a18258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b008201f6a100d90102818258202152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db125840973b06957b62b5d3149ef6a6137ffc4f4e1305eedbb706cb80c1bcaee815eafb6aa52d14384905d4799726eb4fa4d139c39a292adda60818f17da9d1c75e220df5f6",
"expected": {
"predicate": "UnelectedCommitteeVoters"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"title": "vote cast by a committee hot key at protocol version 11 with an empty committee",
"description": "A hot credential no member authorized, reported as an unelected voter rather than an unknown one.",
"network": "preprod",
"eraHistory": {
"$ref": "common/eraHistory/preprod-conway.json"
},
"protocolParameters": {
"$ref": "common/protocolParameters/preprod-conway-v10.json",
"$override": {
"version": {
"major": 11,
"minor": 0
}
}
},
"initialState": {
"utxo": [
{
"input": "825820c14fc165f73be892fa7f9be6ea80949c2ac86fe38ff70dbb8f41de266b8c21ce00",
"output": "a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a004c4b40"
}
],
"pools": [],
"accounts": [],
"dreps": [],
"committee": [],
"proposals": [
{
"id": "8258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b00",
"validUntil": 100
}
],
"governanceActivity": {
"consecutiveDormantEpochs": 0
},
"pots": {
"treasury": 0,
"reserves": 0
}
},
"point": {
"slot": 0,
"transactionIndex": 0
},
"transaction": "84a500d9010281825820c14fc165f73be892fa7f9be6ea80949c2ac86fe38ff70dbb8f41de266b8c21ce000181a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a00493e00021a00030d400f0013a18200581c93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8a18258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b008201f6a100d90102818258202152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db125840f15048fe89e5c3ad5c1a5af9d5149d4d5b5ec03952c956dfe637db88bfe443af0cc6382ac9cccfd465e8bea685c714735703b81f3e34576b9813bde88dce8d0df5f6",
"expected": {
"predicate": "UnelectedCommitteeVoters"
}
}
Loading
Loading