-
Notifications
You must be signed in to change notification settings - Fork 18
/
dynamic_typing.rs
214 lines (184 loc) · 8.25 KB
/
dynamic_typing.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
//! Dynamic Typing utilities for Tuxedo runtimes
//!
//! # Motivation
//!
//! In Tuxedo, UTXOs are like envelopes that store to later be opened and computed on.
//! These data can be of many different types depending on which Tuxedo pieces may
//! operate on them. And different Tuxedo runtimes may support different data types.
//!
//! In order to support type safety as well as serialization, we must do some dynamic
//! type checking when a UTXO is taken out of storage to ensure that the data is decoded
//! to the same type from which it was encoded. This is important because it will occasionally
//! be the case that serialized data may validly decode to more than one type.
//!
//! # Example Attack
//!
//! To understand the importance of the type checking, consider a concrete Tuxedo runtime that
//! has both a bitcoin-like cryptocurrency and a crypto-kitties-like NFT game. Imagine a user
//! spends a moderate amount of money breeding cryptokitties until they have an Attack Kitty.
//! And Attack Kitty is one whose serialized data has the property that it would also validly
//! decode into a coin in the cryptocurrency and spent accordingly. In the worst case (best for
//! the attacker) the value of the coin would exceed the value spent breeding the cryptokitties.
//!
//! # Methodology
//!
//! To solve this problem we associate a four-byte type identifier with each data type that can
//! be stored in a UTXO. When a UTXO is stored, the type identifier is stored along with the
//! serialized data. When the UTXO is later read from storage, the type identifier is checked
//! against the type into which the data is being decoded. Currently this read-time checking
//! is the job of the piece developer, although that may be able to improve in the future.
//!
//! # Comparison with `sp_std::any`
//!
//! The Rust standard library, and also the `sp-std` crate offer utilities for dynamic typing
//! as well. We have considered and are still considering using that crate instead of these
//! custom utilities.
//!
//! ## In favor of `sp_std::any`
//!
//! * The compiler guarantees unique type ids for every type, whereas this utility
//! requires the developer to avoid collisions (hopefully macros can improve this slightly)
//! * Using that crate would be less code for Tuxedo developers to maintain
//!
//! ## In favor of this custom utility
//!
//! * `sp_std::any` does not make the type_id public. This makes it impossible to encode it
//! and store it along with the serialized data which is the whole point. This could be _hacked_
//! around by, for example, hashing the Debug strong, but that is ugly
use parity_scale_codec::{Decode, Encode};
use scale_info::TypeInfo;
use serde::{Deserialize, Serialize};
use sp_std::vec::Vec;
/// A piece of encoded data with a type id associated
/// Strongly typed data can be extracted
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
pub struct DynamicallyTypedData {
pub data: Vec<u8>,
pub type_id: [u8; 4],
}
/// A trait that must be implemented for any data that can be contained in a UTXO.
/// It is not recommended to implement this trait directly for primitive types, but rather to
/// use the newtype pattern: https://doc.rust-lang.org/book/ch19-04-advanced-types.html.
/// Using a new type allows strong type disambiguation between bespoke use-cases in which
/// the same primitive may be stored.
pub trait UtxoData: Encode + Decode {
//TODO Not great that it is up to the runtime dev to enforce uniqueness
// Maybe macros can help... Doesn't frame somehow pass info about the string in construct runtime to the pallet-level storage items?
/// A unique identifier for this type. For now choosing this value and making sure it
/// really is unique is the problem of the developer. Ideally this would be better.
const TYPE_ID: [u8; 4];
}
impl DynamicallyTypedData {
/// Extracts strongly typed data from an Output, iff the output contains the type of data
/// specified. If the contained data is not the specified type, or decoding fails, this errors.
pub fn extract<T: UtxoData>(&self) -> Result<T, DynamicTypingError> {
// The first four bytes represent the type id that that was encoded. If they match the type
// we are trying to decode into, we continue, otherwise we error out.
if self.type_id == <T as UtxoData>::TYPE_ID {
T::decode(&mut &self.data[..]).map_err(|_| DynamicTypingError::DecodingFailed)
} else {
Err(DynamicTypingError::WrongType)
}
}
}
/// Errors that can occur when casting dynamically typed data into strongly typed data.
#[derive(Debug, PartialEq, Eq)]
pub enum DynamicTypingError {
/// The data provided was not of the target decoding type.
WrongType,
/// Although the types matched, the data could not be decoded with the SCALE codec.
DecodingFailed,
}
impl sp_std::fmt::Display for DynamicTypingError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::WrongType => write!(f, "dynamic type does not match extraction target"),
Self::DecodingFailed => write!(
f,
"failed to decode dynamically typed data with scale codec"
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DynamicTypingError {}
//TODO, I tried replacing the extract method above with this impl,
// but it conflicts with something in core, that I don't understand.
// Extracts strongly typed data from dynamically typed data.
// impl<U: UtxoData> TryInto<U> for TypedData {
// type Error = DynamicTypingError;
// fn try_into(self) -> Result<U, Self::Error> {
// todo!()
// }
// }
// Packages strongly typed data with a dynamic typing tag
// probably for storage in a UTXO `Output`.
impl<T: UtxoData> From<T> for DynamicallyTypedData {
fn from(value: T) -> Self {
Self {
data: value.encode(),
type_id: T::TYPE_ID,
}
}
}
pub mod testing {
use super::*;
/// A bogus data type for use in tests.
///
/// When writing tests for individual Tuxedo pieces, developers
/// need to make sure that the piece properly sanitizes the dynamically
/// typed data that is passed into its verifiers.
/// This type is used to represent incorrectly typed data.
#[derive(Encode, Decode, PartialEq, Eq, Debug)]
pub struct Bogus;
impl UtxoData for Bogus {
const TYPE_ID: [u8; 4] = *b"bogs";
}
}
#[cfg(test)]
mod tests {
use super::*;
use testing::Bogus;
/// A simple type that implements UtxoData and just wraps a single u8.
/// Used to test the extraction logic.
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
struct Byte(u8);
impl UtxoData for Byte {
const TYPE_ID: [u8; 4] = *b"byte";
}
#[test]
fn extract_works() {
let original_b = Byte(4);
let dynamically_typed_b: DynamicallyTypedData = original_b.clone().into();
let extracted_b = dynamically_typed_b.extract::<Byte>();
assert_eq!(extracted_b, Ok(original_b));
}
#[test]
fn extract_wrong_type() {
let original_b = Byte(4);
let dynamically_typed_b: DynamicallyTypedData = original_b.clone().into();
let extracted_b = dynamically_typed_b.extract::<Bogus>();
assert_eq!(extracted_b, Err(DynamicTypingError::WrongType));
}
#[test]
fn extract_decode_fails() {
let original_b = Byte(4);
let mut dynamically_typed_b: DynamicallyTypedData = original_b.clone().into();
// Change the encoded bytes so they no longer decode correctly.
dynamically_typed_b.data = Vec::new();
let extracted_b = dynamically_typed_b.extract::<Byte>();
assert_eq!(extracted_b, Err(DynamicTypingError::DecodingFailed));
}
#[test]
fn display_wrong_type_error() {
let actual = format!("{}", DynamicTypingError::WrongType);
let expected = String::from("dynamic type does not match extraction target");
assert_eq!(actual, expected);
}
#[test]
fn display_decoding_failed_error() {
let actual = format!("{}", DynamicTypingError::DecodingFailed);
let expected = String::from("failed to decode dynamically typed data with scale codec");
assert_eq!(actual, expected);
}
}