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.
This commit is contained in:
Kayos 2026-05-06 08:05:55 -07:00
parent 507fd9da15
commit 8091abd1b4

View file

@ -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<u8>) -> 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
}