use anyhow::anyhow;
use thiserror::Error;
use crate::{
crypto::{
multisig,
signature::{self, PublicKey, Signature},
},
types::{
address,
address::{Address, SignatureAddressSpec},
token,
},
};
pub const SIGNATURE_CONTEXT_BASE: &[u8] = b"oasis-runtime-sdk/tx: v0";
pub const LATEST_TRANSACTION_VERSION: u16 = 1;
#[derive(Debug, Error)]
pub enum Error {
#[error("unsupported version")]
UnsupportedVersion,
#[error("malformed transaction: {0}")]
MalformedTransaction(anyhow::Error),
}
#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
pub enum AuthProof {
#[cbor(rename = "signature")]
Signature(Signature),
#[cbor(rename = "multisig")]
Multisig(multisig::SignatureSetOwned),
#[cbor(rename = "module")]
Module(String),
}
#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
#[cbor(no_default)]
pub struct UnverifiedTransaction(pub Vec<u8>, pub Vec<AuthProof>);
impl UnverifiedTransaction {
pub fn verify(self) -> Result<Transaction, Error> {
let body: Transaction =
cbor::from_slice(&self.0).map_err(|e| Error::MalformedTransaction(e.into()))?;
body.validate_basic()?;
if self.1.len() != body.auth_info.signer_info.len() {
return Err(Error::MalformedTransaction(anyhow!(
"unexpected number of auth proofs. expected {} but found {}",
body.auth_info.signer_info.len(),
self.1.len()
)));
}
let ctx = signature::context::get_chain_context_for(SIGNATURE_CONTEXT_BASE);
let mut public_keys = vec![];
let mut signatures = vec![];
for (si, auth_proof) in body.auth_info.signer_info.iter().zip(self.1.iter()) {
let (mut batch_pks, mut batch_sigs) = si.address_spec.batch(auth_proof)?;
public_keys.append(&mut batch_pks);
signatures.append(&mut batch_sigs);
}
PublicKey::verify_batch_multisig(&ctx, &self.0, &public_keys, &signatures)
.map_err(|e| Error::MalformedTransaction(e.into()))?;
Ok(body)
}
}
#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
#[cbor(no_default)]
pub struct Transaction {
#[cbor(rename = "v")]
pub version: u16,
pub call: Call,
#[cbor(rename = "ai")]
pub auth_info: AuthInfo,
}
impl Transaction {
pub fn validate_basic(&self) -> Result<(), Error> {
if self.version != LATEST_TRANSACTION_VERSION {
return Err(Error::UnsupportedVersion);
}
if self.auth_info.signer_info.is_empty() {
return Err(Error::MalformedTransaction(anyhow!(
"transaction has no signers"
)));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
#[repr(u8)]
#[cbor(with_default)]
pub enum CallFormat {
#[default]
Plain = 0,
EncryptedX25519DeoxysII = 1,
}
#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
pub struct Call {
#[cbor(optional)]
pub format: CallFormat,
#[cbor(optional)]
pub method: String,
pub body: cbor::Value,
#[cbor(optional, rename = "ro")]
pub read_only: bool,
}
impl Default for Call {
fn default() -> Self {
Self {
format: Default::default(),
method: Default::default(),
body: cbor::Value::Simple(cbor::SimpleValue::NullValue),
read_only: false,
}
}
}
#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
pub struct AuthInfo {
#[cbor(rename = "si")]
pub signer_info: Vec<SignerInfo>,
pub fee: Fee,
#[cbor(optional)]
pub not_before: Option<u64>,
#[cbor(optional)]
pub not_after: Option<u64>,
}
#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)]
pub struct Fee {
pub amount: token::BaseUnits,
#[cbor(optional)]
pub gas: u64,
#[cbor(optional)]
pub consensus_messages: u32,
}
impl Fee {
pub fn gas_price(&self) -> u128 {
self.amount
.amount()
.checked_div(self.gas.into())
.unwrap_or_default()
}
}
#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
pub enum CallerAddress {
#[cbor(rename = "address")]
Address(Address),
#[cbor(rename = "eth_address")]
EthAddress([u8; 20]),
}
impl CallerAddress {
pub fn address(&self) -> Address {
match self {
CallerAddress::Address(address) => *address,
CallerAddress::EthAddress(address) => Address::new(
address::ADDRESS_V0_SECP256K1ETH_CONTEXT,
address::ADDRESS_V0_VERSION,
address.as_ref(),
),
}
}
pub fn zeroized(&self) -> Self {
match self {
CallerAddress::Address(_) => CallerAddress::Address(Default::default()),
CallerAddress::EthAddress(_) => CallerAddress::EthAddress(Default::default()),
}
}
}
#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
pub enum AddressSpec {
#[cbor(rename = "signature")]
Signature(SignatureAddressSpec),
#[cbor(rename = "multisig")]
Multisig(multisig::Config),
#[cbor(skip)]
Internal(CallerAddress),
}
impl AddressSpec {
pub fn address(&self) -> Address {
match self {
AddressSpec::Signature(spec) => Address::from_sigspec(spec),
AddressSpec::Multisig(config) => Address::from_multisig(config.clone()),
AddressSpec::Internal(caller) => caller.address(),
}
}
pub fn caller_address(&self) -> CallerAddress {
match self {
AddressSpec::Signature(SignatureAddressSpec::Secp256k1Eth(pk)) => {
CallerAddress::EthAddress(pk.to_eth_address().try_into().unwrap())
}
AddressSpec::Internal(caller) => caller.clone(),
_ => CallerAddress::Address(self.address()),
}
}
pub fn batch(&self, auth_proof: &AuthProof) -> Result<(Vec<PublicKey>, Vec<Signature>), Error> {
match (self, auth_proof) {
(AddressSpec::Signature(spec), AuthProof::Signature(signature)) => {
Ok((vec![spec.public_key()], vec![signature.clone()]))
}
(AddressSpec::Multisig(config), AuthProof::Multisig(signature_set)) => Ok(config
.batch(signature_set)
.map_err(|e| Error::MalformedTransaction(e.into()))?),
(AddressSpec::Signature(_), AuthProof::Multisig(_)) => {
Err(Error::MalformedTransaction(anyhow!(
"transaction signer used a single signature, but auth proof was multisig"
)))
}
(AddressSpec::Multisig(_), AuthProof::Signature(_)) => {
Err(Error::MalformedTransaction(anyhow!(
"transaction signer used multisig, but auth proof was a single signature"
)))
}
(AddressSpec::Internal(_), _) => Err(Error::MalformedTransaction(anyhow!(
"transaction signer used internal address spec"
))),
(_, AuthProof::Module(_)) => Err(Error::MalformedTransaction(anyhow!(
"module-controlled decoding flag in auth proof list"
))),
}
}
}
#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
#[cbor(no_default)]
pub struct SignerInfo {
pub address_spec: AddressSpec,
pub nonce: u64,
}
impl SignerInfo {
pub fn new_sigspec(spec: SignatureAddressSpec, nonce: u64) -> Self {
Self {
address_spec: AddressSpec::Signature(spec),
nonce,
}
}
pub fn new_multisig(config: multisig::Config, nonce: u64) -> Self {
Self {
address_spec: AddressSpec::Multisig(config),
nonce,
}
}
}
#[derive(Clone, Debug, cbor::Encode, cbor::Decode)]
pub enum CallResult {
#[cbor(rename = "ok")]
Ok(cbor::Value),
#[cbor(rename = "fail")]
Failed {
module: String,
code: u32,
#[cbor(optional)]
message: String,
},
#[cbor(rename = "unknown")]
Unknown(cbor::Value),
}
impl Default for CallResult {
fn default() -> Self {
Self::Unknown(cbor::Value::Simple(cbor::SimpleValue::NullValue))
}
}
impl CallResult {
pub fn is_success(&self) -> bool {
!matches!(self, CallResult::Failed { .. })
}
}
#[cfg(any(test, feature = "test"))]
impl CallResult {
pub fn unwrap(self) -> cbor::Value {
match self {
Self::Ok(v) | Self::Unknown(v) => v,
Self::Failed {
module,
code,
message,
} => panic!("{module} reported failure with code {code}: {message}"),
}
}
pub fn into_call_result(self) -> Option<crate::module::CallResult> {
Some(match self {
Self::Ok(v) => crate::module::CallResult::Ok(v),
Self::Failed {
module,
code,
message,
} => crate::module::CallResult::Failed {
module,
code,
message,
},
Self::Unknown(_) => return None,
})
}
}
#[cfg(test)]
mod test {
use crate::types::token::{BaseUnits, Denomination};
use super::*;
#[test]
fn test_fee_gas_price() {
let fee = Fee {
amount: Default::default(),
gas: 0,
consensus_messages: 0,
};
assert_eq!(0, fee.gas_price(), "empty fee - gas price should be zero",);
let fee = Fee {
amount: Default::default(),
gas: 100,
consensus_messages: 0,
};
assert_eq!(
0,
fee.gas_price(),
"empty fee amount - gas price should be zero",
);
let fee = Fee {
amount: BaseUnits::new(1_000, Denomination::NATIVE),
gas: 0,
consensus_messages: 0,
};
assert_eq!(0, fee.gas_price(), "empty fee 0 - gas price should be zero",);
let fee = Fee {
amount: BaseUnits::new(1_000, Denomination::NATIVE),
gas: 10_000,
consensus_messages: 0,
};
assert_eq!(
0,
fee.gas_price(),
"non empty fee - gas price should be zero"
);
let fee = Fee {
amount: BaseUnits::new(1_000, Denomination::NATIVE),
gas: 500,
consensus_messages: 0,
};
assert_eq!(2, fee.gas_price(), "non empty fee - gas price should match");
}
}