1use 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
16pub const SIGNATURE_CONTEXT_BASE: &[u8] = b"oasis-runtime-sdk/tx: v0";
18pub const LATEST_TRANSACTION_VERSION: u16 = 1;
20
21#[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#[derive(Clone, Default, Debug, cbor::Encode, cbor::Decode)]
36pub enum AuthProof {
37 #[cbor(rename = "signature")]
39 Signature(Signature),
40 #[cbor(rename = "multisig")]
42 Multisig(multisig::SignatureSetOwned),
43 #[cbor(rename = "module")]
46 Module(String),
47
48 #[cbor(skip)]
50 #[default]
51 Invalid,
52}
53
54#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
56#[cbor(no_default)]
57pub struct UnverifiedTransaction(pub Vec<u8>, pub Vec<AuthProof>);
58
59impl UnverifiedTransaction {
60 pub fn verify(self) -> Result<Transaction, Error> {
62 let body: Transaction =
64 cbor::from_slice(&self.0).map_err(|e| Error::MalformedTransaction(e.into()))?;
65 body.validate_basic()?;
66
67 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 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
92pub struct TransactionSigner {
94 auth_info: AuthInfo,
95 ut: UnverifiedTransaction,
96}
97
98impl TransactionSigner {
99 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 fn allocate_proofs(&mut self) {
112 if !self.ut.1.is_empty() {
113 return;
114 }
115
116 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 *ap = AuthProof::Multisig(vec![None; cfg.signers.len()]);
126 }
127 _ => continue,
128 }
129 }
130 }
131
132 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 pub fn finalize(self) -> UnverifiedTransaction {
178 self.ut
179 }
180}
181
182#[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 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 pub fn prepare_for_signing(self) -> TransactionSigner {
215 TransactionSigner::new(self)
216 }
217
218 pub fn fee_gas(&self) -> u64 {
220 self.auth_info.fee.gas
221 }
222
223 pub fn set_fee_gas(&mut self, gas: u64) {
225 self.auth_info.fee.gas = gas;
226 }
227
228 pub fn fee_amount(&self) -> &token::BaseUnits {
230 &self.auth_info.fee.amount
231 }
232
233 pub fn set_fee_amount(&mut self, amount: token::BaseUnits) {
235 self.auth_info.fee.amount = amount;
236 }
237
238 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 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 pub fn append_auth_signature(&mut self, spec: SignatureAddressSpec, nonce: u64) {
257 self.append_signer_info(AddressSpec::Signature(spec), nonce);
258 }
259
260 pub fn append_auth_multisig(&mut self, cfg: multisig::Config, nonce: u64) {
263 self.append_signer_info(AddressSpec::Multisig(cfg), nonce);
264 }
265
266 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
282#[repr(u8)]
283#[cbor(with_default)]
284pub enum CallFormat {
285 #[default]
287 Plain = 0,
288 EncryptedX25519DeoxysII = 1,
290}
291
292impl CallFormat {
293 pub fn is_encrypted(&self) -> bool {
295 match self {
296 Self::Plain => false,
297 Self::EncryptedX25519DeoxysII => true,
298 }
299 }
300}
301
302#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
304pub struct Call {
305 #[cbor(optional)]
307 pub format: CallFormat,
308 #[cbor(optional)]
310 pub method: String,
311 pub body: cbor::Value,
313 #[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#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
334pub struct AuthInfo {
335 #[cbor(rename = "si")]
337 pub signer_info: Vec<SignerInfo>,
338 pub fee: Fee,
340 #[cbor(optional)]
342 pub not_before: Option<u64>,
343 #[cbor(optional)]
345 pub not_after: Option<u64>,
346}
347
348#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
350pub struct Fee {
351 pub amount: token::BaseUnits,
353 #[cbor(optional)]
355 pub gas: u64,
356 #[cbor(optional)]
359 pub consensus_messages: u32,
360 #[cbor(optional)]
362 pub proxy: Option<FeeProxy>,
363}
364
365impl Fee {
366 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#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
377pub struct FeeProxy {
378 pub module: String,
380 pub id: Vec<u8>,
382}
383
384#[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 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 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#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
413pub enum AddressSpec {
414 #[cbor(rename = "signature")]
416 Signature(SignatureAddressSpec),
417 #[cbor(rename = "multisig")]
419 Multisig(multisig::Config),
420
421 #[cbor(skip)]
423 Internal(CallerAddress),
424}
425
426impl AddressSpec {
427 pub fn public_key(&self) -> Option<PublicKey> {
429 match self {
430 AddressSpec::Signature(spec) => Some(spec.public_key()),
431 _ => None,
432 }
433 }
434
435 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 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 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#[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 pub fn new_sigspec(spec: SignatureAddressSpec, nonce: u64) -> Self {
499 Self {
500 address_spec: AddressSpec::Signature(spec),
501 nonce,
502 }
503 }
504
505 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#[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 pub fn is_success(&self) -> bool {
542 !matches!(self, CallResult::Failed { .. })
543 }
544
545 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}