1pub mod v0;
2
3use crate::data_contract::TokenContractPosition;
4use crate::group::action_event::GroupActionEvent;
5use crate::group::group_action::v0::GroupActionV0;
6#[cfg(feature = "json-conversion")]
7use crate::serialization::JsonConvertible;
8#[cfg(feature = "value-conversion")]
9use crate::serialization::ValueConvertible;
10use crate::ProtocolError;
11use bincode::{Decode, DecodeUntrusted, Encode};
12use platform_serialization_derive::{
13 PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
14};
15use platform_value::Identifier;
16#[cfg(feature = "serde-conversion")]
17use serde::{Deserialize, Serialize};
18
19#[cfg_attr(
20 all(feature = "json-conversion", feature = "serde-conversion"),
21 derive(JsonConvertible)
22)]
23#[derive(
24 Debug,
25 PartialEq,
26 PartialOrd,
27 Clone,
28 Eq,
29 Encode,
30 Decode,
31 PlatformDeserializeTrusted,
32 PlatformDeserializeUntrusted,
33 PlatformSerialize,
34 DecodeUntrusted,
35)]
36#[cfg_attr(
37 feature = "serde-conversion",
38 derive(Serialize, Deserialize),
39 serde(tag = "$formatVersion")
40)]
41#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
42#[platform_serialize(limit = 262144, unversioned)] pub enum GroupAction {
73 #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))]
74 V0(GroupActionV0),
75}
76
77pub trait GroupActionAccessors {
78 fn contract_id(&self) -> Identifier;
79
80 fn proposer_id(&self) -> Identifier;
81 fn token_contract_position(&self) -> TokenContractPosition;
82 fn event(&self) -> &GroupActionEvent;
83}
84impl GroupActionAccessors for GroupAction {
85 fn contract_id(&self) -> Identifier {
86 match self {
87 GroupAction::V0(inner) => inner.contract_id(),
88 }
89 }
90
91 fn proposer_id(&self) -> Identifier {
92 match self {
93 GroupAction::V0(inner) => inner.proposer_id(),
94 }
95 }
96
97 fn token_contract_position(&self) -> TokenContractPosition {
98 match self {
99 GroupAction::V0(inner) => inner.token_contract_position(),
100 }
101 }
102
103 fn event(&self) -> &GroupActionEvent {
104 match self {
105 GroupAction::V0(inner) => inner.event(),
106 }
107 }
108}
109
110#[cfg(test)]
115mod deserialize_limit_tests {
116 use super::*;
117 use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention;
118 use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem;
119 use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction;
120 use crate::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient;
121 use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType;
122 use crate::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0;
123 use crate::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution;
124 use crate::serialization::{
125 PlatformDeserializableTrusted, PlatformDeserializableUntrusted, PlatformSerializable,
126 };
127 use crate::tests::fixtures::get_token_conventions_with_localizations_fixture;
128 use crate::tokens::token_event::TokenEvent;
129 use crate::tokens::token_pricing_schedule::TokenPricingSchedule;
130 use platform_version::version::{PlatformVersion, PLATFORM_VERSIONS};
131 use std::collections::BTreeMap;
132 use platform_serialization::PLATFORM_BINCODE_CONFIG;
133
134 const PREVIOUS_BUDGET: usize = 100_000;
137
138 type ActionBuilder = fn(usize) -> GroupAction;
140
141 fn action_with_event(event: TokenEvent) -> GroupAction {
142 GroupAction::V0(GroupActionV0 {
143 contract_id: Identifier::new([1; 32]),
144 proposer_id: Identifier::new([2; 32]),
145 token_contract_position: 0,
146 event: GroupActionEvent::TokenEvent(event),
147 })
148 }
149
150 fn conventions_change(conventions: TokenConfigurationConvention) -> GroupAction {
151 action_with_event(TokenEvent::ConfigUpdate(
152 TokenConfigurationChangeItem::Conventions(conventions),
153 None,
154 ))
155 }
156
157 fn localizations_action(entries: usize) -> GroupAction {
158 conventions_change(get_token_conventions_with_localizations_fixture(entries))
159 }
160
161 fn shortest_u64_map(entries: usize) -> BTreeMap<u64, u64> {
164 (0..entries as u64).map(|key| (key, 0)).collect()
165 }
166
167 fn set_prices_action(entries: usize) -> GroupAction {
168 action_with_event(TokenEvent::ChangePriceForDirectPurchase(
169 Some(TokenPricingSchedule::SetPrices(shortest_u64_map(entries))),
170 None,
171 ))
172 }
173
174 fn stepwise_action(entries: usize) -> GroupAction {
175 action_with_event(TokenEvent::ConfigUpdate(
176 TokenConfigurationChangeItem::PerpetualDistribution(Some(
177 TokenPerpetualDistribution::V0(TokenPerpetualDistributionV0 {
178 distribution_type: RewardDistributionType::BlockBasedDistribution {
179 interval: 1,
180 function: DistributionFunction::Stepwise(shortest_u64_map(entries)),
181 },
182 distribution_recipient: TokenDistributionRecipient::ContractOwner,
183 }),
184 )),
185 None,
186 ))
187 }
188
189 fn note_action(length: usize) -> GroupAction {
190 action_with_event(TokenEvent::Mint(
191 1,
192 Identifier::new([3; 32]),
193 Some("a".repeat(length)),
194 ))
195 }
196
197 fn encode(action: &GroupAction) -> Vec<u8> {
198 action
199 .serialize_to_bytes()
200 .expect("expected to encode the action")
201 }
202
203 fn largest_fitting(build: ActionBuilder, max_bytes: usize) -> GroupAction {
206 let (mut fits, mut too_many) = (0, max_bytes);
208 while too_many - fits > 1 {
209 let entries = (fits + too_many) / 2;
210 if encode(&build(entries)).len() <= max_bytes {
211 fits = entries;
212 } else {
213 too_many = entries;
214 }
215 }
216 build(fits)
217 }
218
219 fn decodes_under_previous_budget(bytes: &[u8]) -> bool {
222 let config = PLATFORM_BINCODE_CONFIG.with_limit::<PREVIOUS_BUDGET>();
223 bincode::decode_from_slice_untrusted::<GroupAction, _>(bytes, config).is_ok()
224 }
225
226 fn assert_decodes_with_every_decoder(bytes: &[u8], action: &GroupAction, shape: &str) {
229 let untrusted = GroupAction::deserialize_from_bytes_untrusted(bytes)
230 .unwrap_or_else(|e| panic!("{shape}: untrusted decode failed: {e}"));
231 assert_eq!(&untrusted, action, "{shape}: untrusted decode");
232 let trusted = GroupAction::deserialize_from_bytes_trusted(bytes)
233 .unwrap_or_else(|e| panic!("{shape}: trusted decode failed: {e}"));
234 assert_eq!(&trusted, action, "{shape}: trusted decode");
235 let trusted_no_limit = GroupAction::deserialize_from_bytes_trusted_no_limit(bytes)
236 .unwrap_or_else(|e| panic!("{shape}: trusted decode without a limit failed: {e}"));
237 assert_eq!(
238 &trusted_no_limit, action,
239 "{shape}: trusted decode without a limit"
240 );
241 }
242
243 #[test]
248 fn should_decode_a_conventions_change_with_1250_localizations() {
249 let conventions = get_token_conventions_with_localizations_fixture(1_250);
250 assert!(conventions
251 .validate_localizations(PlatformVersion::latest())
252 .expect("expected to validate the localizations")
253 .is_valid());
254 let action = conventions_change(conventions);
255 let bytes = encode(&action);
256 assert_eq!(bytes.len(), 16_325);
257
258 assert!(!decodes_under_previous_budget(&bytes));
259 assert_decodes_with_every_decoder(&bytes, &action, "1,250 localizations");
260 }
261
262 #[test]
270 fn should_decode_the_largest_storable_action_of_each_container_shape() {
271 let max_bytes = PLATFORM_VERSIONS
272 .iter()
273 .map(|platform_version| platform_version.system_limits.max_state_transition_size)
274 .max()
275 .expect("expected platform versions") as usize;
276 let shapes: [(&str, ActionBuilder, bool); 4] = [
277 ("localizations", localizations_action, false),
278 ("SetPrices", set_prices_action, true),
279 ("Stepwise", stepwise_action, true),
280 ("note", note_action, true),
281 ];
282 for (shape, build, decoded_under_previous_budget) in shapes {
283 let action = largest_fitting(build, max_bytes);
284 let bytes = encode(&action);
285 assert!(
286 bytes.len() > max_bytes - 16,
287 "{shape}: {} bytes",
288 bytes.len()
289 );
290 assert_eq!(
291 decodes_under_previous_budget(&bytes),
292 decoded_under_previous_budget,
293 "{shape}: previous budget"
294 );
295 assert_decodes_with_every_decoder(&bytes, &action, shape);
296 }
297 }
298
299 #[test]
303 fn should_refuse_a_localizations_length_prefix_beyond_the_budget() {
304 let empty = encode(&localizations_action(0));
307 let mut buf = empty[..empty.len() - 3].to_vec();
308 buf.extend_from_slice(
309 &bincode::encode_to_vec(1_000_000u64, PLATFORM_BINCODE_CONFIG).unwrap(),
310 );
311
312 let err = GroupAction::deserialize_from_bytes_untrusted(&buf)
313 .expect_err("oversized length prefix must be rejected");
314 assert!(
315 matches!(err, ProtocolError::MaxEncodedBytesReachedError { .. }),
316 "unexpected error: {err}"
317 );
318 }
319
320 #[test]
323 fn rejects_note_length_prefix_beyond_budget_without_allocating() {
324 let config = PLATFORM_BINCODE_CONFIG;
325 let mut buf = Vec::new();
326 buf.extend_from_slice(&bincode::encode_to_vec(0u32, config).unwrap());
328 buf.extend_from_slice(&[0u8; 32]);
329 buf.extend_from_slice(&[0u8; 32]);
330 buf.extend_from_slice(&bincode::encode_to_vec(0u16, config).unwrap());
331 buf.extend_from_slice(&bincode::encode_to_vec(0u32, config).unwrap());
333 buf.extend_from_slice(&bincode::encode_to_vec(2u32, config).unwrap());
334 buf.extend_from_slice(&[0u8; 32]);
335 buf.push(1);
336 buf.extend_from_slice(&bincode::encode_to_vec(8_000_000_000u64, config).unwrap());
338
339 let err = GroupAction::deserialize_from_bytes_untrusted(&buf)
340 .expect_err("oversized length prefix must be rejected");
341 assert!(
342 matches!(err, ProtocolError::MaxEncodedBytesReachedError { .. }),
343 "unexpected error: {err}"
344 );
345 }
346}