From 8091abd1b45c716453b7360def29311cf4600c0d Mon Sep 17 00:00:00 2001 From: Kayos Date: Wed, 6 May 2026 08:05:55 -0700 Subject: [PATCH] pallas-txbuilder: voting_procedures rejects empty CBOR maps AUDIT-2026-05-06 M-4 fix from the aldabra Phase 3-6 audit. NonEmptyKeyValuePairs::decode in pallas-codec accepts `0xa0` (empty CBOR map) because the upstream empty-map check is commented out. That decoded value re-encodes correctly and passes through pallas-txbuilder, but the resulting Conway tx fails ledger validation at submit time with a non-obvious error. Add a debug_assert_ne! on the builder method input + clear doc note warning callers to omit the field instead of passing an empty map. Release builds pass through (no overhead); dev/test builds catch accidental empty-map calls with a clear panic message. The pre-existing aldabra build_signed_drep_vote_cast always constructs a non-empty map so it doesn't trip this; the guard is for future callers. --- pallas-txbuilder/src/transaction/model.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pallas-txbuilder/src/transaction/model.rs b/pallas-txbuilder/src/transaction/model.rs index bc4e4c4..8e20281 100644 --- a/pallas-txbuilder/src/transaction/model.rs +++ b/pallas-txbuilder/src/transaction/model.rs @@ -433,7 +433,22 @@ impl StagingTransaction { /// VotingProcedure>>` map and pass the encoded bytes here. /// /// Used for DRep / SPO / committee voting on Conway governance actions. + /// + /// **Empty-map footgun**: Conway ledger expects this field to be + /// omitted (i.e. `None`) when there are no votes. Passing an empty + /// CBOR map (`0xa0`) gets through pallas-codec's + /// `NonEmptyKeyValuePairs::decode` (the upstream empty-map check is + /// commented out) but the resulting tx fails ledger validation at + /// submit time. **Don't call this builder if the map is empty — + /// just omit it.** The debug_assert below catches accidental empty- + /// map calls in dev/test builds. pub fn voting_procedures(mut self, cbor_bytes: Vec) -> Self { + debug_assert_ne!( + cbor_bytes.as_slice(), + &[0xa0u8], + "voting_procedures called with empty CBOR map — Conway ledger \ + rejects this; omit the field instead", + ); self.voting_procedures = Some(cbor_bytes); self }