Skip to content
This repository was archived by the owner on Dec 4, 2024. It is now read-only.

refactor: Split serialization from Codec into separate modules #414

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"debug.allowBreakpointsEverywhere": true,
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true
}
}
1 change: 1 addition & 0 deletions stacks-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ serde = { workspace = true, features = ["derive"] }
sha2.workspace = true
strum = { workspace = true, features = ["derive"] }
thiserror.workspace = true
anyhow.workspace = true

[dev-dependencies]
hex.workspace = true
Expand Down
7 changes: 7 additions & 0 deletions stacks-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use strum::{Display, EnumIter, EnumString, FromRepr};
use thiserror::Error;
use uint::Uint256;

use serialize::SerializationError;

/// Module for interacting with stacks addresses
pub mod address;
/// Module for c32 encoding and decoding
Expand All @@ -25,6 +27,8 @@ pub mod uint;
pub mod utils;
pub mod wallet;

pub mod serialize;

/// Error type for the stacks-core library
#[derive(Error, Debug)]
pub enum StacksError {
Expand All @@ -49,6 +53,9 @@ pub enum StacksError {
#[error("Codec error: {0}")]
/// Codec error
CodecError(#[from] CodecError),
/// Serialization error
#[error("Serialization error: {0}")]
SerializationError(#[from] SerializationError),
#[error("Invalid data: {0}")]
/// Invalid data
InvalidData(String),
Expand Down
63 changes: 63 additions & 0 deletions stacks-core/src/serialize/amount.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! A module providing serialization for [BitcoinAmount].

use std::io;

use bdk::bitcoin::Amount as BitcoinAmount;

use super::{DeserializeBytes, SerializeBytes};

impl SerializeBytes for BitcoinAmount {
fn write_buffer<WritableBuffer: io::Write>(
&self,
buffer: &mut WritableBuffer,
) -> io::Result<()> {
buffer.write_all(&self.to_sat().to_be_bytes())
}
}

impl DeserializeBytes for BitcoinAmount {
fn read_buffer<ReadableBuffer: io::Read>(
buffer: &mut ReadableBuffer,
) -> io::Result<Self>
where
Self: Sized,
{
let mut amount_bytes = [0; 8];
buffer.read_exact(&mut amount_bytes)?;

Ok(Self::from_sat(u64::from_be_bytes(amount_bytes)))
}
}

#[cfg(test)]
mod tests {
use super::*;

use std::io::Cursor;

#[test]
fn should_serialize_amount() {
let amount = BitcoinAmount::from_sat(10_000);
let mut serialized_amount = vec![];

amount.serialize(&mut serialized_amount).unwrap();

let expected_serialized_amount =
hex::decode("0000000000002710").unwrap();

assert_eq!(serialized_amount, expected_serialized_amount);
}

#[test]
fn should_deserialize_amount() {
let mut serialized_amount =
Cursor::new(hex::decode("0000000000002710").unwrap());

let deserialized_amount =
BitcoinAmount::deserialize(&mut serialized_amount).unwrap();

let expected_deserialized_amount = BitcoinAmount::from_sat(10_000);

assert_eq!(deserialized_amount, expected_deserialized_amount);
}
}
75 changes: 75 additions & 0 deletions stacks-core/src/serialize/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! A module for serialization and deserialization of primitives used by sBTC.

use std::io;
use thiserror::Error;

use crate::StacksResult;

pub mod amount;
pub mod recoverable_signature;

/// Errors resulting from serialization.
#[derive(Error, Debug)]
pub enum SerializationError {
/// Error resulting from failing to read or write data from a buffer.
#[error("Could not serialize or deserialize: {0}")]
IoError(#[from] io::Error),
}

/// A trait for serializing data into a writable buffer.
///
/// [SerializeBytes] provides a mechanism to convert an instance of a type
/// into a series of bytes that can be written to an output stream or buffer.
/// This is useful when precise control of the byte structure is required during
/// serialization.
pub trait SerializeBytes {
/// Serializes the invoking object into the provided writable buffer.
fn serialize<WritableBuffer: io::Write>(
&self,
dest: &mut WritableBuffer,
) -> StacksResult<()> {
self.write_buffer(dest)
.map_err(|err| SerializationError::IoError(err).into())
}

/// Writes data to a buffer, used for deserialization.
///
/// # ⚠️ Do not use directly
///
/// This function is not supposed to be called directly. Doing so can cause
/// unexpected effects like improper error handling.
fn write_buffer<WritableBuffer: io::Write>(
&self,
buffer: &mut WritableBuffer,
) -> io::Result<()>;
}

/// A trait for deserializing data from a readable buffer.
///
/// [DeserializeBytes] provides a mechanism to convert a series of bytes that can be
/// read from an output stream or buffer into an instance of a type. This is useful
/// when precise control of the byte structure is required during serialization.
pub trait DeserializeBytes {
/// Deserializes an object from the provided readable buffer.
fn deserialize<ReadableBuffer: io::Read>(
src: &mut ReadableBuffer,
) -> StacksResult<Self>
where
Self: Sized,
{
Self::read_buffer(src)
.map_err(|err| SerializationError::IoError(err).into())
}

/// Reads data from a buffer, used for deserialization.
///
/// # ⚠️ Do not use directly
///
/// This function is not supposed to be called directly. Doing so can cause
/// unexpected effects like improper error handling.
fn read_buffer<ReadableBuffer: io::Read>(
buffer: &mut ReadableBuffer,
) -> io::Result<Self>
where
Self: Sized;
}
142 changes: 142 additions & 0 deletions stacks-core/src/serialize/recoverable_signature.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//! A module providing serialization for [BitcoinRecoverableSignature]

use std::io;

use bdk::bitcoin::secp256k1::ecdsa::{
RecoverableSignature as BitcoinRecoverableSignature, RecoveryId,
};

use super::{DeserializeBytes, SerializeBytes};

impl SerializeBytes for BitcoinRecoverableSignature {
fn write_buffer<WritableBuffer: std::io::Write>(
&self,
buffer: &mut WritableBuffer,
) -> std::io::Result<()> {
let (id, signature) = self.serialize_compact();

let id: u8 = id.to_i32().try_into().unwrap();

buffer.write_all(&[id])?;
buffer.write_all(&signature)
}
}

impl DeserializeBytes for BitcoinRecoverableSignature {
fn read_buffer<ReadableBuffer: std::io::Read>(
buffer: &mut ReadableBuffer,
) -> std::io::Result<Self>
where
Self: Sized,
{
let mut id = [0; 1];
buffer.read_exact(&mut id)?;

let id = RecoveryId::from_i32(id[0] as i32)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;

let mut signature_buffer = [0; 64];
buffer.read_exact(&mut signature_buffer)?;

Self::from_compact(&signature_buffer, id)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
}
}

#[cfg(test)]
mod tests {
use super::*;

use std::io::Cursor;

use bdk::bitcoin::secp256k1::{Message, Secp256k1, SecretKey};

use crate::StacksError;

#[test]
fn should_serialize_recoverable_signature() {
let signature = get_recoverable_signature().unwrap();
let mut serialized_signature = vec![];

signature.serialize(&mut serialized_signature).unwrap();

let expected_serialized_signature = hex::decode("0119874ebfb457c08cedb5ebf01fe13bf4b6ac216b6f4044763ad95a69022bf1ba3cdba26d7ebb695a7144c8de4ba672dddfc602ffa9e62a745d8f7e4206ae6a93").unwrap();

assert_eq!(serialized_signature, expected_serialized_signature);
}

#[test]
fn should_deserialize_recoverable_signature() {
let mut serialized_signature = Cursor::new(
hex::decode("0119874ebfb457c08cedb5ebf01fe13bf4b6ac216b6f4044763ad95a69022bf1ba3cdba26d7ebb695a7144c8de4ba672dddfc602ffa9e62a745d8f7e4206ae6a93").unwrap()
);

let signature =
BitcoinRecoverableSignature::deserialize(&mut serialized_signature)
.unwrap();

let expected_signature = get_recoverable_signature().unwrap();

assert_eq!(signature, expected_signature);
}

#[test]
fn should_fail_deserialize_recoverable_signature_with_recovery_id_bytes_out_of_bounds(
) {
let mut invalid_serialized_signature = Cursor::new(vec![4]);

let result = BitcoinRecoverableSignature::deserialize(
&mut invalid_serialized_signature,
);

match result {
Err(StacksError::SerializationError(_)) => {}
Err(e) => {
panic!("Expected SerializationError, but got {:?}", e)
}
Ok(_) => panic!("Expected invalid recovery ID error, but got Ok"),
}
}

#[test]
fn should_fail_deserialize_recoverable_signature_with_signature_bytes_non_ecdsa(
) {
let mut invalid_serialized_signature = vec![0; 65];

invalid_serialized_signature[0] = 1;

for i in 1..65 {
invalid_serialized_signature[i] = 255;
}

let result = BitcoinRecoverableSignature::deserialize(
&mut Cursor::new(invalid_serialized_signature),
);

match result {
Err(StacksError::SerializationError(_)) => {}
Err(e) => panic!("Expected SerializationError, got {:?}", e),
Ok(_) => panic!("Expected invalid signature error, but got Ok"),
}
}

fn get_recoverable_signature() -> anyhow::Result<BitcoinRecoverableSignature>
{
let secp = Secp256k1::new();

let secret_key_bytes = hex::decode(
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
)?;

let secret_key = SecretKey::from_slice(&secret_key_bytes)?;

let message = Message::from_slice(&mut hex::decode(
"1bf9ad7ce49adf6cbc707a689b6e17653151e95c1cd8a53f9fce54d3d51a2a24",
)?)?;

let recoverable_signature =
secp.sign_ecdsa_recoverable(&message, &secret_key);

Ok(recoverable_signature)
}
}