diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e6c4cf2..b55b80f3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Other guiding principles: ### Changed - **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][]) ### Fixed @@ -275,6 +276,7 @@ Other guiding principles: [#912]: https://github.com/pragma-org/amaru/issues/912 [#915]: https://github.com/pragma-org/amaru/issues/915 [#923]: https://github.com/pragma-org/amaru/issues/923 +[#924]: https://github.com/pragma-org/amaru/issues/924 [#928]: https://github.com/pragma-org/amaru/issues/928 [#929]: https://github.com/pragma-org/amaru/issues/929 [#932]: https://github.com/pragma-org/amaru/issues/932 @@ -330,3 +332,4 @@ Other guiding principles: [#1109]: https://github.com/pragma-org/amaru/pull/1109 [#1118]: https://github.com/pragma-org/amaru/pull/1118 [#1138]: https://github.com/pragma-org/amaru/pull/1138 +[#1139]: https://github.com/pragma-org/amaru/pull/1139 diff --git a/crates/amaru-ledger/src/rules/transaction/phase_one/fixture.rs b/crates/amaru-ledger/src/rules/transaction/phase_one/fixture.rs index 5dabd2c54..4cadead14 100644 --- a/crates/amaru-ledger/src/rules/transaction/phase_one/fixture.rs +++ b/crates/amaru-ledger/src/rules/transaction/phase_one/fixture.rs @@ -239,6 +239,7 @@ pub(super) enum Predicate { ConwayTxRefScriptsSizeTooBig, ConwayWdrlNotDelegatedToDRep, FeeTooSmallUTxO, + GovActionsDoNotExist, IncorrectDepositDELEG, IncorrectTotalCollateralField, ConwayTreasuryValueMismatch, @@ -342,6 +343,9 @@ impl From for Predicate { PhaseOneError::VotingProcedures(InvalidVotingProcedures::VotersDoNotExist(_)) => { Predicate::VotersDoNotExist } + PhaseOneError::VotingProcedures(InvalidVotingProcedures::GovActionsDoNotExist(_)) => { + Predicate::GovActionsDoNotExist + } PhaseOneError::ValueNotPreserved(_) => Predicate::ValueNotConservedUTxO, PhaseOneError::Certificates(InvalidCertificates::StakeCredentialInvalidPoolDelegation(ref e)) => match e { DelegateError::UnknownSource(_) => Predicate::StakeCredentialInvalidPoolDelegation, diff --git a/crates/amaru-ledger/src/rules/transaction/phase_one/voting_procedures.rs b/crates/amaru-ledger/src/rules/transaction/phase_one/voting_procedures.rs index 69022ed53..5a47342e7 100644 --- a/crates/amaru-ledger/src/rules/transaction/phase_one/voting_procedures.rs +++ b/crates/amaru-ledger/src/rules/transaction/phase_one/voting_procedures.rs @@ -26,6 +26,9 @@ use crate::context::{CommitteeSlice, DRepsSlice, PoolsSlice, ProposalsSlice, Wit pub enum InvalidVotingProcedures { #[error("voters do not exist: {0:?}")] VotersDoNotExist(BTreeSet), + + #[error("governance actions do not exist: {0:?}")] + GovActionsDoNotExist(BTreeSet), } pub(crate) fn execute( @@ -38,13 +41,29 @@ where if let Some(voting_procedures) = voting_procedures { let voting_procedures = voting_procedures.into_iter().collect::>(); - let unknown_voters = - voting_procedures.keys().filter(|voter| !exists(context, voter)).cloned().collect::>(); + let mut unknown_voters = BTreeSet::new(); + let mut unknown_proposals = BTreeSet::new(); + + for (voter, votes) in voting_procedures.iter() { + if !exists(context, voter) { + unknown_voters.insert(voter.clone()); + } + + for (proposal_id, _) in votes.iter() { + if !ProposalsSlice::exists(context, proposal_id) { + unknown_proposals.insert(*proposal_id); + } + } + } if !unknown_voters.is_empty() { return Err(InvalidVotingProcedures::VotersDoNotExist(unknown_voters)); } + if !unknown_proposals.is_empty() { + return Err(InvalidVotingProcedures::GovActionsDoNotExist(unknown_proposals)); + } + voting_procedures.into_iter().enumerate().for_each(|(index, (voter, votes))| { match voter.owner() { StakeCredential::ScriptHash(hash) => { diff --git a/crates/amaru-ledger/tests/data/phase-one/fail/GovActionsDoNotExist/0.json b/crates/amaru-ledger/tests/data/phase-one/fail/GovActionsDoNotExist/0.json new file mode 100644 index 000000000..bc45f9ec3 --- /dev/null +++ b/crates/amaru-ledger/tests/data/phase-one/fail/GovActionsDoNotExist/0.json @@ -0,0 +1,50 @@ +{ + "title": "vote cast on a governance action absent from the proposals state", + "description": "A registered DRep naming a proposal id that no proposal in state carries.", + "network": "preprod", + "eraHistory": { + "$ref": "common/eraHistory/preprod-conway.json" + }, + "protocolParameters": { + "$ref": "common/protocolParameters/preprod-conway-v10.json" + }, + "initialState": { + "utxo": [ + { + "input": "8258207ae12fe7076ec75319e0ecac782bcb6d828bb6a4baa49eada7116b47a80d449200", + "output": "a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a004c4b40" + } + ], + "pools": [], + "accounts": [], + "dreps": [ + { + "credential": "8200581c93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8", + "deposit": 500000000, + "registeredAt": { + "transaction": { + "slot": 0, + "transactionIndex": 0 + }, + "certificateIndex": 0 + }, + "validUntil": 1000 + } + ], + "committee": [], + "proposals": [ + "8258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b00" + ], + "governanceActivity": { + "consecutiveDormantEpochs": 0 + } + }, + "point": { + "slot": 0, + "transactionIndex": 0 + }, + "transaction": "84a500d90102818258207ae12fe7076ec75319e0ecac782bcb6d828bb6a4baa49eada7116b47a80d4492000181a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a00493e00021a00030d400f0013a18202581c93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8a1825820bbd665a051206b767522b19427cced4e0420a050852f7ba1c875c9ec1e4983e1008201f6a100d90102818258202152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db1258405c8b74a8926ec66661779cff7512c53ef14cc243fb20907a1a62c841f9fdaef6d409301c96944bdbd5f355b077894b773717722e8fd7764a1c5fd1020f56540ff5f6", + "expected": { + "predicate": "GovActionsDoNotExist" + } +} diff --git a/crates/amaru-ledger/tests/data/phase-one/fail/GovActionsDoNotExist/1.json b/crates/amaru-ledger/tests/data/phase-one/fail/GovActionsDoNotExist/1.json new file mode 100644 index 000000000..d61246703 --- /dev/null +++ b/crates/amaru-ledger/tests/data/phase-one/fail/GovActionsDoNotExist/1.json @@ -0,0 +1,37 @@ +{ + "title": "vote cast on a governance action whose index does not match any proposal", + "description": "A proposal id sharing the seeded proposal's transaction id but naming a different action index.", + "network": "preprod", + "eraHistory": { + "$ref": "common/eraHistory/preprod-conway.json" + }, + "protocolParameters": { + "$ref": "common/protocolParameters/preprod-conway-v10.json" + }, + "initialState": { + "utxo": [ + { + "input": "825820c25d414957cf674b477d9bec7c295a653f02aee955828f9ebec7a4ece7891f2900", + "output": "a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a004c4b40" + } + ], + "pools": ["93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8"], + "accounts": [], + "dreps": [], + "committee": [], + "proposals": [ + "8258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b00" + ], + "governanceActivity": { + "consecutiveDormantEpochs": 0 + } + }, + "point": { + "slot": 0, + "transactionIndex": 0 + }, + "transaction": "84a500d9010281825820c25d414957cf674b477d9bec7c295a653f02aee955828f9ebec7a4ece7891f29000181a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a00493e00021a00030d400f0013a18204581c93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8a18258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b018201f6a100d90102818258202152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db1258406d6fd3a60b8e7a94ebd0dda9f809aa4adc4c3ea24c4c29b58aae7b7a7c70f0f8543d7c8efb406a5e37ca38d3c340c681aa8e7a7ef5837327d8ecb29d58aba60bf5f6", + "expected": { + "predicate": "GovActionsDoNotExist" + } +} diff --git a/crates/amaru-ledger/tests/data/phase-one/fail/GovActionsDoNotExist/2.json b/crates/amaru-ledger/tests/data/phase-one/fail/GovActionsDoNotExist/2.json new file mode 100644 index 000000000..0b7c45435 --- /dev/null +++ b/crates/amaru-ledger/tests/data/phase-one/fail/GovActionsDoNotExist/2.json @@ -0,0 +1,43 @@ +{ + "title": "vote cast by a committee member on a governance action that does not exist", + "description": "An authorized committee hot key naming an absent proposal id.", + "network": "preprod", + "eraHistory": { + "$ref": "common/eraHistory/preprod-conway.json" + }, + "protocolParameters": { + "$ref": "common/protocolParameters/preprod-conway-v10.json" + }, + "initialState": { + "utxo": [ + { + "input": "82582033e3dfb56c093ef6a5e0a85e186f6c4cba64004d1c21d06d868e1168672b9cdc00", + "output": "a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a004c4b40" + } + ], + "pools": [], + "accounts": [], + "dreps": [], + "committee": [ + { + "coldCredential": "8200581cecc3093b061a9ed426cd9956161fc48d4ed1ac58dc4aaed7e1ce1830", + "hotCredential": "8200581c93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8", + "validUntil": 200 + } + ], + "proposals": [ + "8258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b00" + ], + "governanceActivity": { + "consecutiveDormantEpochs": 0 + } + }, + "point": { + "slot": 0, + "transactionIndex": 0 + }, + "transaction": "84a500d901028182582033e3dfb56c093ef6a5e0a85e186f6c4cba64004d1c21d06d868e1168672b9cdc000181a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a00493e00021a00030d400f0013a18200581c93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8a1825820bbd665a051206b767522b19427cced4e0420a050852f7ba1c875c9ec1e4983e1008201f6a100d90102818258202152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db1258407c0b8db38f40877a619d45a2da54831f6d6bec42d67d3fa70b4db8a1dca933eaf07e1adf533e05e439c404f15d57cba594cc6bc2155bfc7552d79526896e3002f5f6", + "expected": { + "predicate": "GovActionsDoNotExist" + } +} diff --git a/crates/amaru-ledger/tests/data/phase-one/fail/GovActionsDoNotExist/3.json b/crates/amaru-ledger/tests/data/phase-one/fail/GovActionsDoNotExist/3.json new file mode 100644 index 000000000..b1a037acd --- /dev/null +++ b/crates/amaru-ledger/tests/data/phase-one/fail/GovActionsDoNotExist/3.json @@ -0,0 +1,50 @@ +{ + "title": "vote cast on two governance actions of which only one exists", + "description": "A single voter casting one ballot on the seeded proposal and one on an absent id.", + "network": "preprod", + "eraHistory": { + "$ref": "common/eraHistory/preprod-conway.json" + }, + "protocolParameters": { + "$ref": "common/protocolParameters/preprod-conway-v10.json" + }, + "initialState": { + "utxo": [ + { + "input": "8258202b648a1f906db52cc04eac0a58116b1ed17c7bacab29a2f592d325b12281cafc00", + "output": "a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a004c4b40" + } + ], + "pools": [], + "accounts": [], + "dreps": [ + { + "credential": "8200581c93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8", + "deposit": 500000000, + "registeredAt": { + "transaction": { + "slot": 0, + "transactionIndex": 0 + }, + "certificateIndex": 0 + }, + "validUntil": 1000 + } + ], + "committee": [], + "proposals": [ + "8258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b00" + ], + "governanceActivity": { + "consecutiveDormantEpochs": 0 + } + }, + "point": { + "slot": 0, + "transactionIndex": 0 + }, + "transaction": "84a500d90102818258202b648a1f906db52cc04eac0a58116b1ed17c7bacab29a2f592d325b12281cafc000181a200581d6093c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8011a00493e00021a00030d400f0013a18202581c93c191b1094746961f6f00fba27f3d8eff6a66490baf806d4e179fd8a28258207e2b9d5f1a3c7e5b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b008201f6825820bbd665a051206b767522b19427cced4e0420a050852f7ba1c875c9ec1e4983e1008201f6a100d90102818258202152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db125840f608a4bbfed1cf60d841586def7f826ba2d65241612d4b5e0512a1285024aae057abf43a3e38db8e57cbe23fe3b95f09a6633030139959a72bcda8f2122c5e0df5f6", + "expected": { + "predicate": "GovActionsDoNotExist" + } +} diff --git a/crates/amaru-ledger/tests/data/rules-conformance.failures.toml b/crates/amaru-ledger/tests/data/rules-conformance.failures.toml index e10761b83..ae7ab4e55 100644 --- a/crates/amaru-ledger/tests/data/rules-conformance.failures.toml +++ b/crates/amaru-ledger/tests/data/rules-conformance.failures.toml @@ -29,7 +29,6 @@ # PassEpoch event that this harness skips, so the empty prev-gov-action-id can't be rejected here. "conway/fail-gov-empty-prevgovid-after-the-first-constitution-was-enacted/0" = "Expected failure, got success" "conway/fail-gov-expired-gov-actions/0" = "Expected failure, got success" -"conway/fail-gov-non-existent-gov-actions/0" = "Expected failure, got success" "conway/fail-gov-policy-is-respected-by-proposals/0" = "Expected failure, got success" "conway/fail-govcert-resigning-a-non-cc-key/0" = "Expected failure, got success" "conway/fail-utxos-alwaysfails-plutus-govpolicy-does-not-validate/0" = "Expected failure, got success"