near_primitives/
signable_message.rs

1use borsh::{BorshDeserialize, BorshSerialize};
2use near_crypto::{Signature, Signer};
3use near_primitives_core::hash::hash;
4use near_primitives_core::types::AccountId;
5use near_schema_checker_lib::ProtocolSchema;
6
7// These numbers are picked to be compatible with the current protocol and how
8// transactions are defined in it. Introducing this is no protocol change. This
9// is just a forward-looking implementation detail of meta transactions.
10//
11// We plan to establish a standard with NEP-461 that makes this an official
12// specification in the wider ecosystem. Note that NEP-461 should not change the
13// protocol in any way, unless we have to change meta transaction implementation
14// details to adhere to the future standard.
15// [NEP-461](https://github.com/near/NEPs/pull/461)
16//
17// TODO: consider making these public once there is an approved standard.
18const MIN_ON_CHAIN_DISCRIMINANT: u32 = 1 << 30;
19const MAX_ON_CHAIN_DISCRIMINANT: u32 = (1 << 31) - 1;
20const MIN_OFF_CHAIN_DISCRIMINANT: u32 = 1 << 31;
21const MAX_OFF_CHAIN_DISCRIMINANT: u32 = u32::MAX;
22
23// NEPs currently included in the scheme
24const NEP_366_META_TRANSACTIONS: u32 = 366;
25
26/// Used to distinguish message types that are sign by account keys, to avoid an
27/// abuse of signed messages as something else.
28///
29/// This prefix must be at the first four bytes of a message body that is
30/// signed under this signature scheme.
31///
32/// The scheme is a draft introduced to avoid security issues with the
33/// implementation of meta transactions (NEP-366) but will eventually be
34/// standardized with NEP-461 that solves the problem more generally.
35#[derive(
36    Debug,
37    Clone,
38    Copy,
39    PartialEq,
40    Eq,
41    PartialOrd,
42    Ord,
43    Hash,
44    BorshSerialize,
45    BorshDeserialize,
46    serde::Serialize,
47    serde::Deserialize,
48    ProtocolSchema,
49)]
50pub struct MessageDiscriminant {
51    /// The unique prefix, serialized in little-endian by borsh.
52    discriminant: u32,
53}
54
55/// A wrapper around a message that should be signed using this scheme.
56///
57/// Only used for constructing a signature, not used to transmit messages. The
58/// discriminant prefix is implicit and should be known by the receiver based on
59/// the context in which the message is received.
60#[derive(BorshSerialize)]
61pub struct SignableMessage<'a, T> {
62    pub discriminant: MessageDiscriminant,
63    pub msg: &'a T,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
67#[non_exhaustive]
68pub enum SignableMessageType {
69    /// A delegate action, intended for a relayer to included it in an action list of a transaction.
70    DelegateAction,
71}
72
73#[derive(thiserror::Error, Debug)]
74#[non_exhaustive]
75pub enum ReadDiscriminantError {
76    #[error("does not fit any known categories")]
77    UnknownMessageType,
78    #[error("NEP {0} does not have a known on-chain use")]
79    UnknownOnChainNep(u32),
80    #[error("NEP {0} does not have a known off-chain use")]
81    UnknownOffChainNep(u32),
82    #[error("discriminant is in the range for transactions")]
83    TransactionFound,
84}
85
86#[derive(thiserror::Error, Debug)]
87#[non_exhaustive]
88pub enum CreateDiscriminantError {
89    #[error("nep number {0} is too big")]
90    NepTooLarge(u32),
91}
92
93impl<'a, T: BorshSerialize> SignableMessage<'a, T> {
94    pub fn new(msg: &'a T, ty: SignableMessageType) -> Self {
95        let discriminant = ty.into();
96        Self { discriminant, msg }
97    }
98
99    pub fn sign(&self, signer: &Signer) -> Signature {
100        let bytes = borsh::to_vec(&self).expect("Failed to deserialize");
101        let hash = hash(&bytes);
102        signer.sign(hash.as_bytes())
103    }
104}
105
106impl MessageDiscriminant {
107    /// Create a discriminant for an on-chain actionable message that was introduced in the specified NEP.
108    ///
109    /// Allows creating discriminants currently unknown in this crate, which can
110    /// be useful to prototype new standards. For example, when the client
111    /// project still relies on an older version of this crate while nightly
112    /// nearcore already supports a new NEP.
113    pub fn new_on_chain(nep: u32) -> Result<Self, CreateDiscriminantError> {
114        // unchecked arithmetic: these are constants
115        if nep > MAX_ON_CHAIN_DISCRIMINANT - MIN_ON_CHAIN_DISCRIMINANT {
116            Err(CreateDiscriminantError::NepTooLarge(nep))
117        } else {
118            Ok(Self {
119                // unchecked arithmetic: just checked range
120                discriminant: MIN_ON_CHAIN_DISCRIMINANT + nep,
121            })
122        }
123    }
124
125    /// Create a discriminant for an off-chain message that was introduced in the specified NEP.
126    ///
127    /// Allows creating discriminants currently unknown in this crate, which can
128    /// be useful to prototype new standards. For example, when the client
129    /// project still relies on an older version of this crate while nightly
130    /// nearcore already supports a new NEP.
131    pub fn new_off_chain(nep: u32) -> Result<Self, CreateDiscriminantError> {
132        // unchecked arithmetic: these are constants
133        if nep > MAX_OFF_CHAIN_DISCRIMINANT - MIN_OFF_CHAIN_DISCRIMINANT {
134            Err(CreateDiscriminantError::NepTooLarge(nep))
135        } else {
136            Ok(Self {
137                // unchecked arithmetic: just checked range
138                discriminant: MIN_OFF_CHAIN_DISCRIMINANT + nep,
139            })
140        }
141    }
142
143    /// Returns the raw integer value of the discriminant as an integer value.
144    pub fn raw_discriminant(&self) -> u32 {
145        self.discriminant
146    }
147
148    /// Whether this discriminant marks a traditional `SignedTransaction`.
149    pub fn is_transaction(&self) -> bool {
150        // Backwards compatibility with transaction that were defined before this standard:
151        // Transaction begins with `AccountId`, which is just a `String` in
152        // borsh serialization, which starts with the length of the underlying
153        // byte vector in little endian u32.
154        // Currently allowed AccountIds are between 2 and 64 bytes.
155        self.discriminant >= AccountId::MIN_LEN as u32
156            && self.discriminant <= AccountId::MAX_LEN as u32
157    }
158
159    /// If this discriminant marks a message intended for on-chain use, return
160    /// the NEP in which the message type was introduced.
161    pub fn on_chain_nep(&self) -> Option<u32> {
162        if self.discriminant < MIN_ON_CHAIN_DISCRIMINANT
163            || self.discriminant > MAX_ON_CHAIN_DISCRIMINANT
164        {
165            None
166        } else {
167            // unchecked arithmetic: just checked it is in range
168            let nep = self.discriminant - MIN_ON_CHAIN_DISCRIMINANT;
169            Some(nep)
170        }
171    }
172
173    /// If this discriminant marks a message intended for off-chain use, return
174    /// the NEP in which the message type was introduced.
175    ///
176    /// clippy: MAX_OFF_CHAIN_DISCRIMINANT currently is u32::MAX which makes the
177    /// comparison pointless, however I think it helps code readability to have
178    /// it spelled out anyway
179    #[allow(clippy::absurd_extreme_comparisons)]
180    pub fn off_chain_nep(&self) -> Option<u32> {
181        if self.discriminant < MIN_OFF_CHAIN_DISCRIMINANT
182            || self.discriminant > MAX_OFF_CHAIN_DISCRIMINANT
183        {
184            None
185        } else {
186            // unchecked arithmetic: just checked it is in range
187            let nep = self.discriminant - MIN_OFF_CHAIN_DISCRIMINANT;
188            Some(nep)
189        }
190    }
191}
192
193impl TryFrom<MessageDiscriminant> for SignableMessageType {
194    type Error = ReadDiscriminantError;
195
196    fn try_from(discriminant: MessageDiscriminant) -> Result<Self, Self::Error> {
197        if discriminant.is_transaction() {
198            Err(Self::Error::TransactionFound)
199        } else if let Some(nep) = discriminant.on_chain_nep() {
200            match nep {
201                NEP_366_META_TRANSACTIONS => Ok(Self::DelegateAction),
202                _ => Err(Self::Error::UnknownOnChainNep(nep)),
203            }
204        } else if let Some(nep) = discriminant.off_chain_nep() {
205            Err(Self::Error::UnknownOffChainNep(nep))
206        } else {
207            Err(Self::Error::UnknownMessageType)
208        }
209    }
210}
211
212impl From<SignableMessageType> for MessageDiscriminant {
213    fn from(ty: SignableMessageType) -> Self {
214        // unwrapping here is ok, we know the constant NEP numbers used are in range
215        match ty {
216            SignableMessageType::DelegateAction => {
217                MessageDiscriminant::new_on_chain(NEP_366_META_TRANSACTIONS).unwrap()
218            }
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use near_crypto::{InMemorySigner, PublicKey};
226
227    use super::*;
228    use crate::action::delegate::{DelegateAction, SignedDelegateAction};
229
230    // happy path for NEP-366 signature
231    #[test]
232    fn nep_366_ok() {
233        let sender_id: AccountId = "alice.near".parse().unwrap();
234        let receiver_id: AccountId = "bob.near".parse().unwrap();
235        let signer = InMemorySigner::test_signer(&sender_id);
236
237        let delegate_action = delegate_action(sender_id, receiver_id, signer.public_key());
238        let signable = SignableMessage::new(&delegate_action, SignableMessageType::DelegateAction);
239        let signed = SignedDelegateAction { signature: signable.sign(&signer), delegate_action };
240
241        assert!(signed.verify());
242    }
243
244    // Try to use a wrong nep number in NEP-366 signature verification.
245    #[test]
246    fn nep_366_wrong_nep() {
247        let sender_id: AccountId = "alice.near".parse().unwrap();
248        let receiver_id: AccountId = "bob.near".parse().unwrap();
249        let signer = InMemorySigner::test_signer(&sender_id);
250
251        let delegate_action = delegate_action(sender_id, receiver_id, signer.public_key());
252        let wrong_nep = 777;
253        let signable = SignableMessage {
254            discriminant: MessageDiscriminant::new_on_chain(wrong_nep).unwrap(),
255            msg: &delegate_action,
256        };
257        let signed = SignedDelegateAction { signature: signable.sign(&signer), delegate_action };
258
259        assert!(!signed.verify());
260    }
261
262    // Try to use a wrong message type in NEP-366 signature verification.
263    #[test]
264    fn nep_366_wrong_msg_type() {
265        let sender_id: AccountId = "alice.near".parse().unwrap();
266        let receiver_id: AccountId = "bob.near".parse().unwrap();
267        let signer = InMemorySigner::test_signer(&sender_id);
268
269        let delegate_action = delegate_action(sender_id, receiver_id, signer.public_key());
270        let correct_nep = 366;
271        // here we use it as an off-chain only signature
272        let wrong_discriminant = MessageDiscriminant::new_off_chain(correct_nep).unwrap();
273        let signable = SignableMessage { discriminant: wrong_discriminant, msg: &delegate_action };
274        let signed = SignedDelegateAction { signature: signable.sign(&signer), delegate_action };
275
276        assert!(!signed.verify());
277    }
278
279    fn delegate_action(
280        sender_id: AccountId,
281        receiver_id: AccountId,
282        public_key: PublicKey,
283    ) -> DelegateAction {
284        let delegate_action = DelegateAction {
285            sender_id,
286            receiver_id,
287            actions: vec![],
288            nonce: 0,
289            max_block_height: 1000,
290            public_key,
291        };
292        delegate_action
293    }
294}