Contracts: emit the lowest slatepack version needed

create_slatepack always serialized to V5, so every slatepack went out V5 and
V4-only peers could not read it. Emit V4 unless the slate carries a payment
proof: only the (opt-in) proof needs the V5-only fields (timestamp/memo) that
V4 cannot represent. The slate's declared version is set to match the emitted
structure so readers select the right variant.

- packer: V5 when payment_proof is present, else V4.
- test: a proof slate round-trips as V5 (memo preserved); a no-proof slate as V4.

Addresses PR #754 review (packer.rs).
This commit is contained in:
jwinterm
2026-06-04 20:14:52 -04:00
parent 15cbbc96e8
commit 62ede419dd
2 changed files with 39 additions and 1 deletions
+26
View File
@@ -267,6 +267,32 @@ pub mod tests {
Ok(()) Ok(())
} }
#[test]
fn create_slatepack_emits_lowest_version() -> Result<(), Error> {
set_local_chain_type(ChainTypes::Mainnet);
let packer = Slatepacker::new(SlatepackerArgs {
sender: None,
recipients: vec![],
dec_key: None,
});
// A slate carrying a payment proof needs V5 (the proof carries memo/timestamp
// that V4 can't represent), and the V5 fields survive the round trip.
let slate_with_proof = populate_test_slate()?;
assert!(slate_with_proof.payment_proof.is_some());
let recovered_v5 = packer.get_slate(&packer.create_slatepack(&slate_with_proof)?)?;
assert_eq!(recovered_v5.version_info.version, 5);
assert!(recovered_v5.payment_proof.unwrap().memo.is_some());
// Without a proof, V4 is sufficient and is emitted for interoperability with V4 peers.
let mut slate_no_proof = populate_test_slate()?;
slate_no_proof.payment_proof = None;
let recovered_v4 = packer.get_slate(&packer.create_slatepack(&slate_no_proof)?)?;
assert_eq!(recovered_v4.version_info.version, 4);
Ok(())
}
#[test] #[test]
fn slatepack_version_v4_v5() -> Result<(), Error> { fn slatepack_version_v4_v5() -> Result<(), Error> {
set_local_chain_type(ChainTypes::Mainnet); set_local_chain_type(ChainTypes::Mainnet);
+13 -1
View File
@@ -94,7 +94,19 @@ impl<'a> Slatepacker<'a> {
/// Create slatepack from slate and args /// Create slatepack from slate and args
pub fn create_slatepack(&self, slate: &Slate) -> Result<Slatepack, Error> { pub fn create_slatepack(&self, slate: &Slate) -> Result<Slatepack, Error> {
let out_slate = VersionedSlate::into_version(slate.clone(), SlateVersion::V5)?; // Emit the lowest version that can represent this slate, for maximum
// interoperability. Only the (opt-in) payment proof needs V5 fields
// (timestamp/memo) that V4 cannot carry; everything else stays V4.
let (version, version_num) = if slate.payment_proof.is_some() {
(SlateVersion::V5, 5)
} else {
(SlateVersion::V4, 4)
};
// Keep the declared version consistent with the emitted structure so readers
// select the right variant.
let mut slate = slate.clone();
slate.version_info.version = version_num;
let out_slate = VersionedSlate::into_version(slate, version)?;
let bin_slate = VersionedBinSlate::try_from(out_slate).map_err(|_| Error::SlatepackSer)?; let bin_slate = VersionedBinSlate::try_from(out_slate).map_err(|_| Error::SlatepackSer)?;
let mut slatepack = Slatepack::default(); let mut slatepack = Slatepack::default();
slatepack.payload = byte_ser::to_bytes(&bin_slate).map_err(|_| Error::SlatepackSer)?; slatepack.payload = byte_ser::to_bytes(&bin_slate).map_err(|_| Error::SlatepackSer)?;