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
133 const PREVIOUS_BUDGET: usize = 100_000;
136
137 type ActionBuilder = fn(usize) -> GroupAction;
139
140 fn action_with_event(event: TokenEvent) -> GroupAction {
141 GroupAction::V0(GroupActionV0 {
142 contract_id: Identifier::new([1; 32]),
143 proposer_id: Identifier::new([2; 32]),
144 token_contract_position: 0,
145 event: GroupActionEvent::TokenEvent(event),
146 })
147 }
148
149 fn conventions_change(conventions: TokenConfigurationConvention) -> GroupAction {
150 action_with_event(TokenEvent::ConfigUpdate(
151 TokenConfigurationChangeItem::Conventions(conventions),
152 None,
153 ))
154 }
155
156 fn localizations_action(entries: usize) -> GroupAction {
157 conventions_change(get_token_conventions_with_localizations_fixture(entries))
158 }
159
160 fn shortest_u64_map(entries: usize) -> BTreeMap<u64, u64> {
163 (0..entries as u64).map(|key| (key, 0)).collect()
164 }
165
166 fn set_prices_action(entries: usize) -> GroupAction {
167 action_with_event(TokenEvent::ChangePriceForDirectPurchase(
168 Some(TokenPricingSchedule::SetPrices(shortest_u64_map(entries))),
169 None,
170 ))
171 }
172
173 fn stepwise_action(entries: usize) -> GroupAction {
174 action_with_event(TokenEvent::ConfigUpdate(
175 TokenConfigurationChangeItem::PerpetualDistribution(Some(
176 TokenPerpetualDistribution::V0(TokenPerpetualDistributionV0 {
177 distribution_type: RewardDistributionType::BlockBasedDistribution {
178 interval: 1,
179 function: DistributionFunction::Stepwise(shortest_u64_map(entries)),
180 },
181 distribution_recipient: TokenDistributionRecipient::ContractOwner,
182 }),
183 )),
184 None,
185 ))
186 }
187
188 fn note_action(length: usize) -> GroupAction {
189 action_with_event(TokenEvent::Mint(
190 1,
191 Identifier::new([3; 32]),
192 Some("a".repeat(length)),
193 ))
194 }
195
196 fn encode(action: &GroupAction) -> Vec<u8> {
197 action
198 .serialize_to_bytes()
199 .expect("expected to encode the action")
200 }
201
202 fn largest_fitting(build: ActionBuilder, max_bytes: usize) -> GroupAction {
205 let (mut fits, mut too_many) = (0, max_bytes);
207 while too_many - fits > 1 {
208 let entries = (fits + too_many) / 2;
209 if encode(&build(entries)).len() <= max_bytes {
210 fits = entries;
211 } else {
212 too_many = entries;
213 }
214 }
215 build(fits)
216 }
217
218 fn decodes_under_previous_budget(bytes: &[u8]) -> bool {
221 let config = bincode::config::standard()
222 .with_big_endian()
223 .with_limit::<PREVIOUS_BUDGET>();
224 bincode::decode_from_slice_untrusted::<GroupAction, _>(bytes, config).is_ok()
225 }
226
227 fn assert_decodes_with_every_decoder(bytes: &[u8], action: &GroupAction, shape: &str) {
230 let untrusted = GroupAction::deserialize_from_bytes_untrusted(bytes)
231 .unwrap_or_else(|e| panic!("{shape}: untrusted decode failed: {e}"));
232 assert_eq!(&untrusted, action, "{shape}: untrusted decode");
233 let trusted = GroupAction::deserialize_from_bytes_trusted(bytes)
234 .unwrap_or_else(|e| panic!("{shape}: trusted decode failed: {e}"));
235 assert_eq!(&trusted, action, "{shape}: trusted decode");
236 let trusted_no_limit = GroupAction::deserialize_from_bytes_trusted_no_limit(bytes)
237 .unwrap_or_else(|e| panic!("{shape}: trusted decode without a limit failed: {e}"));
238 assert_eq!(
239 &trusted_no_limit, action,
240 "{shape}: trusted decode without a limit"
241 );
242 }
243
244 #[test]
249 fn should_decode_a_conventions_change_with_1250_localizations() {
250 let conventions = get_token_conventions_with_localizations_fixture(1_250);
251 assert!(conventions
252 .validate_localizations(PlatformVersion::latest())
253 .expect("expected to validate the localizations")
254 .is_valid());
255 let action = conventions_change(conventions);
256 let bytes = encode(&action);
257 assert_eq!(bytes.len(), 16_325);
258
259 assert!(!decodes_under_previous_budget(&bytes));
260 assert_decodes_with_every_decoder(&bytes, &action, "1,250 localizations");
261 }
262
263 #[test]
271 fn should_decode_the_largest_storable_action_of_each_container_shape() {
272 let max_bytes = PLATFORM_VERSIONS
273 .iter()
274 .map(|platform_version| platform_version.system_limits.max_state_transition_size)
275 .max()
276 .expect("expected platform versions") as usize;
277 let shapes: [(&str, ActionBuilder, bool); 4] = [
278 ("localizations", localizations_action, false),
279 ("SetPrices", set_prices_action, true),
280 ("Stepwise", stepwise_action, true),
281 ("note", note_action, true),
282 ];
283 for (shape, build, decoded_under_previous_budget) in shapes {
284 let action = largest_fitting(build, max_bytes);
285 let bytes = encode(&action);
286 assert!(
287 bytes.len() > max_bytes - 16,
288 "{shape}: {} bytes",
289 bytes.len()
290 );
291 assert_eq!(
292 decodes_under_previous_budget(&bytes),
293 decoded_under_previous_budget,
294 "{shape}: previous budget"
295 );
296 assert_decodes_with_every_decoder(&bytes, &action, shape);
297 }
298 }
299
300 #[test]
304 fn should_refuse_a_localizations_length_prefix_beyond_the_budget() {
305 let empty = encode(&localizations_action(0));
308 let mut buf = empty[..empty.len() - 3].to_vec();
309 buf.extend_from_slice(
310 &bincode::encode_to_vec(1_000_000u64, bincode::config::standard().with_big_endian())
311 .unwrap(),
312 );
313
314 let err = GroupAction::deserialize_from_bytes_untrusted(&buf)
315 .expect_err("oversized length prefix must be rejected");
316 assert!(
317 matches!(err, ProtocolError::MaxEncodedBytesReachedError { .. }),
318 "unexpected error: {err}"
319 );
320 }
321
322 #[test]
325 fn rejects_note_length_prefix_beyond_budget_without_allocating() {
326 let config = bincode::config::standard()
327 .with_big_endian()
328 .with_no_limit();
329 let mut buf = Vec::new();
330 buf.extend_from_slice(&bincode::encode_to_vec(0u32, config).unwrap());
332 buf.extend_from_slice(&[0u8; 32]);
333 buf.extend_from_slice(&[0u8; 32]);
334 buf.extend_from_slice(&bincode::encode_to_vec(0u16, config).unwrap());
335 buf.extend_from_slice(&bincode::encode_to_vec(0u32, config).unwrap());
337 buf.extend_from_slice(&bincode::encode_to_vec(2u32, config).unwrap());
338 buf.extend_from_slice(&[0u8; 32]);
339 buf.push(1);
340 buf.extend_from_slice(&bincode::encode_to_vec(8_000_000_000u64, config).unwrap());
342
343 let err = GroupAction::deserialize_from_bytes_untrusted(&buf)
344 .expect_err("oversized length prefix must be rejected");
345 assert!(
346 matches!(err, ProtocolError::MaxEncodedBytesReachedError { .. }),
347 "unexpected error: {err}"
348 );
349 }
350}