Skip to content

Commit

Permalink
Merge pull request #74 from cspr-rad/feature/full-tx-format
Browse files Browse the repository at this point in the history
Full transaction format with signer integration.
  • Loading branch information
koxu1996 authored Apr 30, 2024
2 parents 06cfb1c + 578f520 commit f5fb2a6
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 1 deletion.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion kairos-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ edition.workspace = true
license.workspace = true

[features]
default = ["crypto-casper"]
default = ["crypto-casper", "tx"]
crypto-casper = ["casper-types"]
tx = ["kairos-tx"]
fs = ["casper-types/std"] # TODO: Change `std` -> `std-fs-io` in the future version.

[lib]

[dependencies]
hex = "0.4"
thiserror = "1"
kairos-tx = { path = "../kairos-tx", optional = true }

# Casper signer implementation.
casper-types = { version = "4", optional = true }
9 changes: 9 additions & 0 deletions kairos-crypto/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,13 @@ pub enum CryptoError {
/// Private key is not provided.
#[error("private key is not provided")]
MissingPrivateKey,

/// Unable to compute transaction hash - invalid data given.
#[cfg(feature = "tx")]
#[error("unable to hash transaction data: {error}")]
TxHashingError { error: String },
/// Signing algorithm is not available in `kairos-tx`.
#[cfg(feature = "tx")]
#[error("algorithm not available in tx format")]
InvalidSigningAlgorithm,
}
44 changes: 44 additions & 0 deletions kairos-crypto/src/implementations/casper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,50 @@ impl CryptoSigner for Signer {

Ok(public_key)
}

#[cfg(feature = "tx")]
fn verify_tx(&self, tx: kairos_tx::asn::Transaction) -> Result<(), CryptoError> {
let tx_hash = tx.payload.hash().map_err(|e| CryptoError::TxHashingError {
error: e.to_string(),
})?;
let signature: Vec<u8> = tx.signature.into();
self.verify(tx_hash, signature)?;

Ok(())
}

#[cfg(feature = "tx")]
fn sign_tx_payload(
&self,
payload: kairos_tx::asn::SigningPayload,
) -> Result<kairos_tx::asn::Transaction, CryptoError> {
// Compute payload signature.
let tx_hash = payload.hash().map_err(|e| CryptoError::TxHashingError {
error: e.to_string(),
})?;
let signature = self.sign(tx_hash)?;

// Prepare public key.
let public_key = self.to_public_key()?;

// Prepare algorithm.
let algorithm = match self.public_key {
PublicKey::Ed25519(_) => Ok(kairos_tx::asn::SigningAlgorithm::CasperEd25519),
PublicKey::Secp256k1(_) => Ok(kairos_tx::asn::SigningAlgorithm::CasperSecp256k1),
_ => Err(CryptoError::InvalidSigningAlgorithm),
}?;

// Build full transaction.
let tx = kairos_tx::asn::Transaction::new(
public_key,
payload,
&tx_hash,
algorithm,
signature.into(),
);

Ok(tx)
}
}

#[cfg(test)]
Expand Down
8 changes: 8 additions & 0 deletions kairos-crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,13 @@ pub trait CryptoSigner {
signature_bytes: U,
) -> Result<(), CryptoError>;

#[cfg(feature = "tx")]
fn verify_tx(&self, tx: kairos_tx::asn::Transaction) -> Result<(), CryptoError>;
#[cfg(feature = "tx")]
fn sign_tx_payload(
&self,
payload: kairos_tx::asn::SigningPayload,
) -> Result<kairos_tx::asn::Transaction, CryptoError>;

fn to_public_key(&self) -> Result<Vec<u8>, CryptoError>;
}
1 change: 1 addition & 0 deletions kairos-tx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ license.workspace = true
[dependencies]
num-traits = "0.2"
rasn = { version = "0.12", default-features = false, features = ["macros"] }
sha2 = "0.10"

[dev-dependencies]
hex = "0.4"
Expand Down
19 changes: 19 additions & 0 deletions kairos-tx/schema.asn
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,28 @@ TxSchema DEFINITIONS AUTOMATIC TAGS ::= BEGIN

-- Basic types.
PublicKey ::= OCTET STRING
Signature ::= OCTET STRING
PayloadHash ::= OCTET STRING
Amount ::= INTEGER (0..18446744073709551615)
Nonce ::= INTEGER (0..18446744073709551615)

-- Full, top-level transaction type.
Transaction ::= SEQUENCE {
publicKey PublicKey,
payload SigningPayload,
hash PayloadHash,
algorithm SigningAlgorithm,
signature Signature,
...
}

-- Support for multiple signing algorithms.
SigningAlgorithm ::= ENUMERATED {
casperSecp256k1 (0),
casperEd25519 (1),
...
}

-- Transaction payload for signing.
SigningPayload ::= SEQUENCE {
nonce Nonce,
Expand Down
114 changes: 114 additions & 0 deletions kairos-tx/src/asn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub use rasn::types::{Integer, OctetString};
use num_traits::cast::ToPrimitive;
use rasn::types::AsnType;
use rasn::{Decode, Encode};
use sha2::Digest;

#[derive(AsnType, Encode, Decode, Debug, Clone)]
#[rasn(delegate)]
Expand All @@ -21,6 +22,12 @@ impl From<PublicKey> for Vec<u8> {
}
}

impl From<Vec<u8>> for PublicKey {
fn from(value: Vec<u8>) -> Self {
PublicKey(OctetString::copy_from_slice(&value))
}
}

impl From<&[u8]> for PublicKey {
fn from(value: &[u8]) -> Self {
PublicKey(OctetString::copy_from_slice(value))
Expand All @@ -33,6 +40,40 @@ impl<const N: usize> From<&[u8; N]> for PublicKey {
}
}

#[derive(AsnType, Encode, Decode, Debug, Clone)]
#[rasn(delegate)]
pub struct Signature(pub(crate) OctetString);

// Converts an ASN.1 decoded signature into raw byte representation.
impl From<Signature> for Vec<u8> {
fn from(value: Signature) -> Self {
value.0.into()
}
}

impl From<Vec<u8>> for Signature {
fn from(value: Vec<u8>) -> Self {
Signature(OctetString::copy_from_slice(&value))
}
}

#[derive(AsnType, Encode, Decode, Debug, Clone)]
#[rasn(delegate)]
pub struct PayloadHash(pub(crate) OctetString);

// Converts an ASN.1 decoded payload hash into raw byte representation.
impl From<PayloadHash> for Vec<u8> {
fn from(value: PayloadHash) -> Self {
value.0.into()
}
}

impl From<&[u8; 32]> for PayloadHash {
fn from(value: &[u8; 32]) -> Self {
PayloadHash(OctetString::copy_from_slice(value))
}
}

#[derive(AsnType, Encode, Decode, Debug, Clone)]
#[rasn(delegate)]
pub struct Amount(pub(crate) Integer);
Expand Down Expand Up @@ -82,6 +123,54 @@ impl From<u64> for Nonce {
}
}

#[derive(AsnType, Encode, Decode, Debug)]
#[non_exhaustive]
pub struct Transaction {
pub public_key: PublicKey, // NOTE: Field name can be different than defined in schema, as only **order** is crucial
pub payload: SigningPayload,
pub hash: PayloadHash,
pub algorithm: SigningAlgorithm,
pub signature: Signature,
}

impl Transaction {
/// Wraps full transaction data for storing.
///
/// CAUTION: This method does NOT perform validity checks - please use
/// `kairos-crypto::sign_tx_payload()` to construct it safely.
pub fn new(
public_key: impl Into<PublicKey>,
payload: SigningPayload,
hash: impl Into<PayloadHash>,
algorithm: SigningAlgorithm,
signature: Signature,
) -> Self {
Self {
public_key: public_key.into(),
payload,
hash: hash.into(),
algorithm,
signature,
}
}

pub fn der_encode(&self) -> Result<Vec<u8>, TxError> {
rasn::der::encode(self).map_err(TxError::EncodeError)
}

pub fn der_decode(value: impl AsRef<[u8]>) -> Result<Self, TxError> {
rasn::der::decode(value.as_ref()).map_err(TxError::DecodeError)
}
}

#[derive(AsnType, Encode, Decode, Debug, PartialEq, Copy, Clone)]
#[rasn(enumerated)]
#[non_exhaustive]
pub enum SigningAlgorithm {
CasperSecp256k1 = 0,
CasperEd25519 = 1,
}

#[derive(AsnType, Encode, Decode, Debug)]
#[non_exhaustive]
pub struct SigningPayload {
Expand Down Expand Up @@ -130,6 +219,15 @@ impl SigningPayload {
pub fn der_decode(value: impl AsRef<[u8]>) -> Result<Self, TxError> {
rasn::der::decode(value.as_ref()).map_err(TxError::DecodeError)
}

// Computes the hash for a transaction.
// Hash is obtained from payload by computing sha256 of DER encoded ASN.1 data.
pub fn hash(&self) -> Result<[u8; 32], TxError> {
let data = self.der_encode()?;
let tx_hash: [u8; 32] = sha2::Sha256::digest(data).into();

Ok(tx_hash)
}
}

#[derive(AsnType, Encode, Decode, Debug)]
Expand Down Expand Up @@ -206,6 +304,22 @@ impl Withdrawal {
}
}

impl TryFrom<&[u8]> for Transaction {
type Error = TxError;

fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Transaction::der_decode(value)
}
}

impl TryFrom<Transaction> for Vec<u8> {
type Error = TxError;

fn try_from(value: Transaction) -> Result<Self, Self::Error> {
value.der_encode()
}
}

impl TryFrom<&[u8]> for SigningPayload {
type Error = TxError;

Expand Down

0 comments on commit f5fb2a6

Please sign in to comment.