oasis_runtime_sdk/types/
transaction.rs

1//! Transaction types.
2use anyhow::anyhow;
3use thiserror::Error;
4
5use crate::{
6    crypto::{
7        multisig,
8        signature::{self, PublicKey, Signature, Signer},
9    },
10    types::{
11        address::{Address, SignatureAddressSpec},
12        token,
13    },
14};
15
16/// Transaction signature domain separation context base.
17pub const SIGNATURE_CONTEXT_BASE: &[u8] = b"oasis-runtime-sdk/tx: v0";
18/// The latest transaction format version.
19pub const LATEST_TRANSACTION_VERSION: u16 = 1;
20
21/// Error.
22#[derive(Debug, Error)]
23pub enum Error {
24    #[error("unsupported version")]
25    UnsupportedVersion,
26    #[error("malformed transaction: {0}")]
27    MalformedTransaction(anyhow::Error),
28    #[error("signer not found in transaction")]
29    SignerNotFound,
30    #[error("failed to sign: {0}")]
31    FailedToSign(#[from] signature::Error),
32}
33
34/// A container for data that authenticates a transaction.
35#[derive(Clone, Default, Debug, cbor::Encode, cbor::Decode)]
36pub enum AuthProof {
37    /// For _signature_ authentication.
38    #[cbor(rename = "signature")]
39    Signature(Signature),
40    /// For _multisig_ authentication.
41    #[cbor(rename = "multisig")]
42    Multisig(multisig::SignatureSetOwned),
43    /// A flag to use module-controlled decoding. The string is an encoding scheme name that a
44    /// module must handle. The scheme name must not be empty.
45    #[cbor(rename = "module")]
46    Module(String),
47
48    /// A non-serializable placeholder value.
49    #[cbor(skip)]
50    #[default]
51    Invalid,
52}
53
54/// An unverified signed transaction.
55#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
56#[cbor(no_default)]
57pub struct UnverifiedTransaction(pub Vec<u8>, pub Vec<AuthProof>);
58
59impl UnverifiedTransaction {
60    /// Verify and deserialize the unverified transaction.
61    pub fn verify(self) -> Result<Transaction, Error> {
62        // Deserialize the inner body.
63        let body: Transaction =
64            cbor::from_slice(&self.0).map_err(|e| Error::MalformedTransaction(e.into()))?;
65        body.validate_basic()?;
66
67        // Basic structure validation.
68        if self.1.len() != body.auth_info.signer_info.len() {
69            return Err(Error::MalformedTransaction(anyhow!(
70                "unexpected number of auth proofs. expected {} but found {}",
71                body.auth_info.signer_info.len(),
72                self.1.len()
73            )));
74        }
75
76        // Verify all signatures.
77        let ctx = signature::context::get_chain_context_for(SIGNATURE_CONTEXT_BASE);
78        let mut public_keys = vec![];
79        let mut signatures = vec![];
80        for (si, auth_proof) in body.auth_info.signer_info.iter().zip(self.1.iter()) {
81            let (mut batch_pks, mut batch_sigs) = si.address_spec.batch(auth_proof)?;
82            public_keys.append(&mut batch_pks);
83            signatures.append(&mut batch_sigs);
84        }
85        PublicKey::verify_batch_multisig(&ctx, &self.0, &public_keys, &signatures)
86            .map_err(|e| Error::MalformedTransaction(e.into()))?;
87
88        Ok(body)
89    }
90}
91
92/// Transaction signer.
93pub struct TransactionSigner {
94    auth_info: AuthInfo,
95    ut: UnverifiedTransaction,
96}
97
98impl TransactionSigner {
99    /// Construct a new transaction signer for the given transaction.
100    pub fn new(tx: Transaction) -> Self {
101        let mut ts = Self {
102            auth_info: tx.auth_info.clone(),
103            ut: UnverifiedTransaction(cbor::to_vec(tx), vec![]),
104        };
105        ts.allocate_proofs();
106
107        ts
108    }
109
110    /// Allocate proof structures based on the specified authentication info in the transaction.
111    fn allocate_proofs(&mut self) {
112        if !self.ut.1.is_empty() {
113            return;
114        }
115
116        // Allocate proof slots.
117        self.ut
118            .1
119            .resize_with(self.auth_info.signer_info.len(), Default::default);
120
121        for (si, ap) in self.auth_info.signer_info.iter().zip(self.ut.1.iter_mut()) {
122            match (&si.address_spec, ap) {
123                (AddressSpec::Multisig(cfg), ap) => {
124                    // Allocate multisig slots.
125                    *ap = AuthProof::Multisig(vec![None; cfg.signers.len()]);
126                }
127                _ => continue,
128            }
129        }
130    }
131
132    /// Sign the transaction and append the signature.
133    ///
134    /// The signer must be specified in the `auth_info` field.
135    pub fn append_sign<S>(&mut self, signer: &S) -> Result<(), Error>
136    where
137        S: Signer + ?Sized,
138    {
139        let ctx = signature::context::get_chain_context_for(SIGNATURE_CONTEXT_BASE);
140        let signature = signer.sign(&ctx, &self.ut.0)?;
141
142        let mut matched = false;
143        for (si, ap) in self.auth_info.signer_info.iter().zip(self.ut.1.iter_mut()) {
144            match (&si.address_spec, ap) {
145                (AddressSpec::Signature(spec), ap) => {
146                    if spec.public_key() != signer.public_key() {
147                        continue;
148                    }
149
150                    matched = true;
151                    *ap = AuthProof::Signature(signature.clone());
152                }
153                (AddressSpec::Multisig(cfg), AuthProof::Multisig(ref mut sigs)) => {
154                    for (i, mss) in cfg.signers.iter().enumerate() {
155                        if mss.public_key != signer.public_key() {
156                            continue;
157                        }
158
159                        matched = true;
160                        sigs[i] = Some(signature.clone());
161                    }
162                }
163                _ => {
164                    return Err(Error::MalformedTransaction(anyhow!(
165                        "malformed address_spec"
166                    )))
167                }
168            }
169        }
170        if !matched {
171            return Err(Error::SignerNotFound);
172        }
173        Ok(())
174    }
175
176    /// Finalize the signing process and return the (signed) unverified transaction.
177    pub fn finalize(self) -> UnverifiedTransaction {
178        self.ut
179    }
180}
181
182/// Transaction.
183#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
184#[cbor(no_default)]
185pub struct Transaction {
186    #[cbor(rename = "v")]
187    pub version: u16,
188
189    pub call: Call,
190
191    #[cbor(rename = "ai")]
192    pub auth_info: AuthInfo,
193}
194
195impl Transaction {
196    /// Create a new (unsigned) transaction.
197    pub fn new<B>(method: &str, body: B) -> Self
198    where
199        B: cbor::Encode,
200    {
201        Self {
202            version: LATEST_TRANSACTION_VERSION,
203            call: Call {
204                format: CallFormat::Plain,
205                method: method.to_string(),
206                body: cbor::to_value(body),
207                ..Default::default()
208            },
209            auth_info: Default::default(),
210        }
211    }
212
213    /// Prepare this transaction for signing.
214    pub fn prepare_for_signing(self) -> TransactionSigner {
215        TransactionSigner::new(self)
216    }
217
218    /// Maximum amount of gas that the transaction can use.
219    pub fn fee_gas(&self) -> u64 {
220        self.auth_info.fee.gas
221    }
222
223    /// Set maximum amount of gas that the transaction can use.
224    pub fn set_fee_gas(&mut self, gas: u64) {
225        self.auth_info.fee.gas = gas;
226    }
227
228    /// Amount of fee to pay for transaction execution.
229    pub fn fee_amount(&self) -> &token::BaseUnits {
230        &self.auth_info.fee.amount
231    }
232
233    /// Set amount of fee to pay for transaction execution.
234    pub fn set_fee_amount(&mut self, amount: token::BaseUnits) {
235        self.auth_info.fee.amount = amount;
236    }
237
238    /// Set a proxy for paying the transaction fee.
239    pub fn set_fee_proxy(&mut self, module: &str, id: &[u8]) {
240        self.auth_info.fee.proxy = Some(FeeProxy {
241            module: module.to_string(),
242            id: id.to_vec(),
243        });
244    }
245
246    /// Append a new transaction signer information to the transaction.
247    pub fn append_signer_info(&mut self, address_spec: AddressSpec, nonce: u64) {
248        self.auth_info.signer_info.push(SignerInfo {
249            address_spec,
250            nonce,
251        })
252    }
253
254    /// Append a new transaction signer information with a signature address specification to the
255    /// transaction.
256    pub fn append_auth_signature(&mut self, spec: SignatureAddressSpec, nonce: u64) {
257        self.append_signer_info(AddressSpec::Signature(spec), nonce);
258    }
259
260    /// Append a new transaction signer information with a multisig address specification to the
261    /// transaction.
262    pub fn append_auth_multisig(&mut self, cfg: multisig::Config, nonce: u64) {
263        self.append_signer_info(AddressSpec::Multisig(cfg), nonce);
264    }
265
266    /// Perform basic validation on the transaction.
267    pub fn validate_basic(&self) -> Result<(), Error> {
268        if self.version != LATEST_TRANSACTION_VERSION {
269            return Err(Error::UnsupportedVersion);
270        }
271        if self.auth_info.signer_info.is_empty() {
272            return Err(Error::MalformedTransaction(anyhow!(
273                "transaction has no signers"
274            )));
275        }
276        Ok(())
277    }
278}
279
280/// Format used for encoding the call (and output) information.
281#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
282#[repr(u8)]
283#[cbor(with_default)]
284pub enum CallFormat {
285    /// Plain text call data.
286    #[default]
287    Plain = 0,
288    /// Encrypted call data using X25519 for key exchange and Deoxys-II for symmetric encryption.
289    EncryptedX25519DeoxysII = 1,
290}
291
292impl CallFormat {
293    /// Whether this call format is end-to-end encrypted.
294    pub fn is_encrypted(&self) -> bool {
295        match self {
296            Self::Plain => false,
297            Self::EncryptedX25519DeoxysII => true,
298        }
299    }
300}
301
302/// Method call.
303#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
304pub struct Call {
305    /// Call format.
306    #[cbor(optional)]
307    pub format: CallFormat,
308    /// Method name.
309    #[cbor(optional)]
310    pub method: String,
311    /// Method body.
312    pub body: cbor::Value,
313    /// Read-only flag.
314    ///
315    /// A read-only call cannot make any changes to runtime state. Any attempt at modifying state
316    /// will result in the call failing.
317    #[cbor(optional, rename = "ro")]
318    pub read_only: bool,
319}
320
321impl Default for Call {
322    fn default() -> Self {
323        Self {
324            format: Default::default(),
325            method: Default::default(),
326            body: cbor::Value::Simple(cbor::SimpleValue::NullValue),
327            read_only: false,
328        }
329    }
330}
331
332/// Transaction authentication information.
333#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
334pub struct AuthInfo {
335    /// Transaction signer information.
336    #[cbor(rename = "si")]
337    pub signer_info: Vec<SignerInfo>,
338    /// Fee payment information.
339    pub fee: Fee,
340    /// Earliest round when the transaction is valid.
341    #[cbor(optional)]
342    pub not_before: Option<u64>,
343    /// Latest round when the transaction is valid.
344    #[cbor(optional)]
345    pub not_after: Option<u64>,
346}
347
348/// Transaction fee.
349#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
350pub struct Fee {
351    /// Amount of base units paid as fee for transaction processing.
352    pub amount: token::BaseUnits,
353    /// Maximum amount of gas paid for.
354    #[cbor(optional)]
355    pub gas: u64,
356    /// Maximum amount of emitted consensus messages paid for. Zero means that up to the maximum
357    /// number of per-batch messages can be emitted.
358    #[cbor(optional)]
359    pub consensus_messages: u32,
360    /// Proxy which has authorized the fees to be paid.
361    #[cbor(optional)]
362    pub proxy: Option<FeeProxy>,
363}
364
365impl Fee {
366    /// Calculates gas price from fee amount and gas.
367    pub fn gas_price(&self) -> u128 {
368        self.amount
369            .amount()
370            .checked_div(self.gas.into())
371            .unwrap_or_default()
372    }
373}
374
375/// Information about a fee proxy.
376#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
377pub struct FeeProxy {
378    /// Module that will handle the proxy payment.
379    pub module: String,
380    /// Module-specific identifier that will handle fee payments for the transaction signer.
381    pub id: Vec<u8>,
382}
383
384/// A caller address.
385#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
386pub enum CallerAddress {
387    #[cbor(rename = "address")]
388    Address(Address),
389    #[cbor(rename = "eth_address")]
390    EthAddress([u8; 20]),
391}
392
393impl CallerAddress {
394    /// Derives the address.
395    pub fn address(&self) -> Address {
396        match self {
397            CallerAddress::Address(address) => *address,
398            CallerAddress::EthAddress(address) => Address::from_eth(address.as_ref()),
399        }
400    }
401
402    /// Maps the caller address to one of the same type but with an all-zero address.
403    pub fn zeroized(&self) -> Self {
404        match self {
405            CallerAddress::Address(_) => CallerAddress::Address(Default::default()),
406            CallerAddress::EthAddress(_) => CallerAddress::EthAddress(Default::default()),
407        }
408    }
409}
410
411/// Common information that specifies an address as well as how to authenticate.
412#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
413pub enum AddressSpec {
414    /// For _signature_ authentication.
415    #[cbor(rename = "signature")]
416    Signature(SignatureAddressSpec),
417    /// For _multisig_ authentication.
418    #[cbor(rename = "multisig")]
419    Multisig(multisig::Config),
420
421    /// For internal child calls (cannot be serialized/deserialized).
422    #[cbor(skip)]
423    Internal(CallerAddress),
424}
425
426impl AddressSpec {
427    /// Returns the public key when the address spec represents a single public key.
428    pub fn public_key(&self) -> Option<PublicKey> {
429        match self {
430            AddressSpec::Signature(spec) => Some(spec.public_key()),
431            _ => None,
432        }
433    }
434
435    /// Derives the address.
436    pub fn address(&self) -> Address {
437        match self {
438            AddressSpec::Signature(spec) => Address::from_sigspec(spec),
439            AddressSpec::Multisig(config) => Address::from_multisig(config.clone()),
440            AddressSpec::Internal(caller) => caller.address(),
441        }
442    }
443
444    /// Derives the caller address.
445    pub fn caller_address(&self) -> CallerAddress {
446        match self {
447            AddressSpec::Signature(SignatureAddressSpec::Secp256k1Eth(pk)) => {
448                CallerAddress::EthAddress(pk.to_eth_address().try_into().unwrap())
449            }
450            AddressSpec::Internal(caller) => caller.clone(),
451            _ => CallerAddress::Address(self.address()),
452        }
453    }
454
455    /// Checks that the address specification and the authentication proof are acceptable.
456    /// Returns vectors of public keys and signatures for batch verification of included signatures.
457    pub fn batch(&self, auth_proof: &AuthProof) -> Result<(Vec<PublicKey>, Vec<Signature>), Error> {
458        match (self, auth_proof) {
459            (AddressSpec::Signature(spec), AuthProof::Signature(signature)) => {
460                Ok((vec![spec.public_key()], vec![signature.clone()]))
461            }
462            (AddressSpec::Multisig(config), AuthProof::Multisig(signature_set)) => Ok(config
463                .batch(signature_set)
464                .map_err(|e| Error::MalformedTransaction(e.into()))?),
465            (AddressSpec::Signature(_), AuthProof::Multisig(_)) => {
466                Err(Error::MalformedTransaction(anyhow!(
467                    "transaction signer used a single signature, but auth proof was multisig"
468                )))
469            }
470            (AddressSpec::Multisig(_), AuthProof::Signature(_)) => {
471                Err(Error::MalformedTransaction(anyhow!(
472                    "transaction signer used multisig, but auth proof was a single signature"
473                )))
474            }
475            (AddressSpec::Internal(_), _) => Err(Error::MalformedTransaction(anyhow!(
476                "transaction signer used internal address spec"
477            ))),
478            (_, AuthProof::Module(_)) => Err(Error::MalformedTransaction(anyhow!(
479                "module-controlled decoding flag in auth proof list"
480            ))),
481            (_, AuthProof::Invalid) => Err(Error::MalformedTransaction(anyhow!(
482                "invalid auth proof in list"
483            ))),
484        }
485    }
486}
487
488/// Transaction signer information.
489#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
490#[cbor(no_default)]
491pub struct SignerInfo {
492    pub address_spec: AddressSpec,
493    pub nonce: u64,
494}
495
496impl SignerInfo {
497    /// Create a new signer info from a signature address specification and nonce.
498    pub fn new_sigspec(spec: SignatureAddressSpec, nonce: u64) -> Self {
499        Self {
500            address_spec: AddressSpec::Signature(spec),
501            nonce,
502        }
503    }
504
505    /// Create a new signer info from a multisig configuration and a nonce.
506    pub fn new_multisig(config: multisig::Config, nonce: u64) -> Self {
507        Self {
508            address_spec: AddressSpec::Multisig(config),
509            nonce,
510        }
511    }
512}
513
514/// Call result.
515#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
516pub enum CallResult {
517    #[cbor(rename = "ok")]
518    Ok(cbor::Value),
519
520    #[cbor(rename = "fail")]
521    Failed {
522        module: String,
523        code: u32,
524
525        #[cbor(optional)]
526        message: String,
527    },
528
529    #[cbor(rename = "unknown")]
530    Unknown(cbor::Value),
531}
532
533impl Default for CallResult {
534    fn default() -> Self {
535        Self::Unknown(cbor::Value::Simple(cbor::SimpleValue::NullValue))
536    }
537}
538
539impl CallResult {
540    /// Check whether the call result indicates a successful operation or not.
541    pub fn is_success(&self) -> bool {
542        !matches!(self, CallResult::Failed { .. })
543    }
544
545    /// Transforms `CallResult` into `anyhow::Result<cbor::Value>`, mapping `Ok(v)` and `Unknown(v)`
546    /// to `Ok(v)` and `Failed` to `Err`.
547    pub fn ok(self) -> anyhow::Result<cbor::Value> {
548        match self {
549            Self::Ok(v) | Self::Unknown(v) => Ok(v),
550            Self::Failed {
551                module,
552                code,
553                message,
554            } => Err(anyhow!(
555                "call failed: module={module} code={code}: {message}"
556            )),
557        }
558    }
559}
560
561#[cfg(any(test, feature = "test"))]
562impl CallResult {
563    pub fn unwrap(self) -> cbor::Value {
564        match self {
565            Self::Ok(v) | Self::Unknown(v) => v,
566            Self::Failed {
567                module,
568                code,
569                message,
570            } => panic!("{module} reported failure with code {code}: {message}"),
571        }
572    }
573
574    pub fn unwrap_failed(self) -> (String, u32) {
575        match self {
576            Self::Ok(_) | Self::Unknown(_) => panic!("call result indicates success"),
577            Self::Failed { module, code, .. } => (module, code),
578        }
579    }
580
581    pub fn into_call_result(self) -> Option<crate::module::CallResult> {
582        Some(match self {
583            Self::Ok(v) => crate::module::CallResult::Ok(v),
584            Self::Failed {
585                module,
586                code,
587                message,
588            } => crate::module::CallResult::Failed {
589                module,
590                code,
591                message,
592            },
593            Self::Unknown(_) => return None,
594        })
595    }
596}
597
598#[cfg(test)]
599mod test {
600    use crate::types::token::{BaseUnits, Denomination};
601
602    use super::*;
603
604    #[test]
605    fn test_fee_gas_price() {
606        let fee = Fee::default();
607        assert_eq!(0, fee.gas_price(), "empty fee - gas price should be zero",);
608
609        let fee = Fee {
610            gas: 100,
611            ..Default::default()
612        };
613        assert_eq!(
614            0,
615            fee.gas_price(),
616            "empty fee amount - gas price should be zero",
617        );
618
619        let fee = Fee {
620            amount: BaseUnits::new(1_000, Denomination::NATIVE),
621            gas: 0,
622            ..Default::default()
623        };
624        assert_eq!(0, fee.gas_price(), "empty fee 0 - gas price should be zero",);
625
626        let fee = Fee {
627            amount: BaseUnits::new(1_000, Denomination::NATIVE),
628            gas: 10_000,
629            ..Default::default()
630        };
631        assert_eq!(
632            0,
633            fee.gas_price(),
634            "non empty fee - gas price should be zero"
635        );
636
637        let fee = Fee {
638            amount: BaseUnits::new(1_000, Denomination::NATIVE),
639            gas: 500,
640            ..Default::default()
641        };
642        assert_eq!(2, fee.gas_price(), "non empty fee - gas price should match");
643    }
644}