Skip to content

Commit

Permalink
Construction preprocess (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
piotr-iohk authored Jan 20, 2025
1 parent 6974b72 commit 0644535
Show file tree
Hide file tree
Showing 26 changed files with 1,009 additions and 47 deletions.
26 changes: 2 additions & 24 deletions src/api/construction_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ use coinbase_mesh::models::{AccountIdentifier, ConstructionDeriveRequest, Constr
use mina_signer::{BaseField, CompressedPubKey};
use o1_utils::FieldHelpers;
use serde_json::{json, Value};
use sha2::Digest;

use crate::{util::DEFAULT_TOKEN_ID, MinaMesh, MinaMeshError};
use crate::{base58::validate_base58_with_checksum, util::DEFAULT_TOKEN_ID, MinaMesh, MinaMeshError};

/// https://github.com/MinaProtocol/mina/blob/985eda49bdfabc046ef9001d3c406e688bc7ec45/src/app/rosetta/lib/construction.ml#L162
impl MinaMesh {
Expand Down Expand Up @@ -88,27 +87,6 @@ fn decode_token_id(metadata: Option<Value>) -> Result<String, MinaMeshError> {
}

/// Validates the token ID format (base58 and checksum)
/// https://github.com/MinaProtocol/mina/blob/985eda49bdfabc046ef9001d3c406e688bc7ec45/src/lib/base58_check/base58_check.ml
fn validate_base58_token_id(token_id: &str) -> Result<(), MinaMeshError> {
// Decode the token ID using base58
let bytes = bs58::decode(token_id)
.with_alphabet(bs58::Alphabet::BITCOIN)
.into_vec()
.map_err(|_| MinaMeshError::MalformedPublicKey("Token_id not valid base58".to_string()))?;

// Check the length (e.g., must include version and checksum)
if bytes.len() < 5 {
return Err(MinaMeshError::MalformedPublicKey("Token_id too short".to_string()));
}

// Split into payload and checksum
let (payload, checksum) = bytes.split_at(bytes.len() - 4);

// Recompute checksum
let computed_checksum = sha2::Sha256::digest(sha2::Sha256::digest(payload));
if &computed_checksum[.. 4] != checksum {
return Err(MinaMeshError::MalformedPublicKey("Token_id checksum mismatch".to_string()));
}

Ok(())
validate_base58_with_checksum(token_id, None)
}
279 changes: 274 additions & 5 deletions src/api/construction_preprocess.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,284 @@
use anyhow::Result;
use coinbase_mesh::models::{ConstructionPreprocessRequest, ConstructionPreprocessResponse};
use coinbase_mesh::models::{ConstructionPreprocessRequest, ConstructionPreprocessResponse, Operation};
use serde_json::{json, Map, Value};

use crate::MinaMesh;
use crate::{
base58::validate_base58_with_checksum,
util::DEFAULT_TOKEN_ID,
MinaMesh, MinaMeshError,
OperationType::{self, *},
PartialReason, PreprocessMetadata, UserCommandType,
};

/// https://github.com/MinaProtocol/mina/blob/985eda49bdfabc046ef9001d3c406e688bc7ec45/src/app/rosetta/lib/construction.ml#L392
impl MinaMesh {
pub async fn construction_preprocess(
&self,
request: ConstructionPreprocessRequest,
) -> Result<ConstructionPreprocessResponse> {
) -> Result<ConstructionPreprocessResponse, MinaMeshError> {
self.validate_network(&request.network_identifier).await?;
Ok(ConstructionPreprocessResponse::new())

let metadata = PreprocessMetadata::from_json(request.metadata)?;
let partial_command = PartialUserCommand::from_operations(&request.operations, metadata)?;

validate_base58_public_key(partial_command.fee_payer.as_str())?;
validate_base58_public_key(partial_command.source.as_str())?;
validate_base58_public_key(partial_command.receiver.as_str())?;

Ok(ConstructionPreprocessResponse {
options: Some(make_response_options(partial_command)),
required_public_keys: Some(vec![]),
})
}
}

fn make_response_options(partial_command: PartialUserCommand) -> Value {
let mut options = Map::new();

options.insert("sender".to_string(), json!(partial_command.fee_payer));
options.insert("receiver".to_string(), json!(partial_command.receiver));
options.insert("token_id".to_string(), json!(partial_command.token));

if let Some(valid_until) = partial_command.valid_until {
options.insert("valid_until".to_string(), json!(valid_until));
}

if let Some(memo) = partial_command.memo {
options.insert("memo".to_string(), json!(memo));
}

json!(options)
}

fn validate_base58_public_key(token_id: &str) -> Result<(), MinaMeshError> {
validate_base58_with_checksum(token_id, None).map_err(|e| MinaMeshError::PublicKeyFormatNotValid(e.to_string()))
}

#[allow(dead_code)]
#[derive(Debug)]
pub struct PartialUserCommand {
pub kind: UserCommandType,
pub fee_payer: String,
pub source: String,
pub receiver: String,
pub fee_token: String,
pub token: String,
pub fee: i64,
pub amount: Option<i64>,
pub valid_until: Option<String>,
pub memo: Option<String>,
}

impl PartialUserCommand {
pub fn from_operations(
operations: &[Operation],
metadata: Option<PreprocessMetadata>,
) -> Result<Self, MinaMeshError> {
let mut errors = Vec::new();
let metadata = metadata.unwrap_or_default();
let valid_until = metadata.valid_until;
let memo = metadata.memo;

match operations.len() {
3 => Self::parse_payment_operations(operations, valid_until, memo).map_err(|err| {
if let MinaMeshError::OperationsNotValid(reasons) = &err {
errors.extend(reasons.clone());
}
MinaMeshError::OperationsNotValid(errors.clone())
}),
2 => Self::parse_delegation_operations(operations, valid_until, memo).map_err(|err| {
if let MinaMeshError::OperationsNotValid(reasons) = &err {
errors.extend(reasons.clone());
}
MinaMeshError::OperationsNotValid(errors.clone())
}),
_ => {
errors.push(PartialReason::LengthMismatch(format!(
"Expected 2 operations for delegation or 3 for payment, got {}",
operations.len()
)));
Err(MinaMeshError::OperationsNotValid(errors))
}
}
}

fn parse_payment_operations(
operations: &[Operation],
valid_until: Option<String>,
memo: Option<String>,
) -> Result<Self, MinaMeshError> {
let mut errors = Vec::new();

let fee_payment = Self::find_operation(operations, FeePayment).inspect_err(|e| {
errors.push(e.clone());
});

let source_dec = Self::find_operation(operations, PaymentSourceDec).inspect_err(|e| {
errors.push(e.clone());
});

let receiver_inc = Self::find_operation(operations, PaymentReceiverInc).inspect_err(|e| {
errors.push(e.clone());
});

if !errors.is_empty() {
return Err(MinaMeshError::OperationsNotValid(errors));
}

let fee_payment = fee_payment.unwrap();
let source_dec = source_dec.unwrap();
let receiver_inc = receiver_inc.unwrap();

let fee_token = Self::token_id_from_operation(fee_payment);
let token = Self::token_id_from_operation(source_dec);

if fee_payment.account != source_dec.account {
errors.push(PartialReason::FeePayerAndSourceMismatch);
}

//Validate source and receiver amounts match
let source_amt = Self::parse_amount_as_i64(source_dec).map_err(|e| {
errors.push(e.clone());
MinaMeshError::OperationsNotValid(errors.clone())
})?;
let receiver_amt = Self::parse_amount_as_i64(receiver_inc).map_err(|e| {
errors.push(e.clone());
MinaMeshError::OperationsNotValid(errors.clone())
})?;
if (source_amt + receiver_amt) != 0 {
errors.push(PartialReason::AmountIncDecMismatch);
}

// Validate the fee
let fee = Self::parse_amount_as_i64(fee_payment).map_err(|e| {
errors.push(e.clone());
MinaMeshError::OperationsNotValid(errors.clone())
})?;
if fee >= 0 {
errors.push(PartialReason::FeeNotNegative);
}

if !errors.is_empty() {
return Err(MinaMeshError::OperationsNotValid(errors));
}

Ok(PartialUserCommand {
kind: UserCommandType::Payment,
fee_payer: Self::address_from_operation(fee_payment),
source: Self::address_from_operation(source_dec),
receiver: Self::address_from_operation(receiver_inc),
fee_token,
token,
fee,
amount: Some(receiver_amt),
valid_until,
memo,
})
}

fn parse_delegation_operations(
operations: &[Operation],
valid_until: Option<String>,
memo: Option<String>,
) -> Result<Self, MinaMeshError> {
let mut errors = Vec::new();

let fee_payment = Self::find_operation(operations, FeePayment).inspect_err(|e| {
errors.push(e.clone());
});

let delegate_change = Self::find_operation(operations, DelegateChange).inspect_err(|e| {
errors.push(e.clone());
});

if !errors.is_empty() {
return Err(MinaMeshError::OperationsNotValid(errors));
}

let fee_payment = fee_payment.unwrap();
let delegate_change = delegate_change.unwrap();

let fee_token = Self::token_id_from_operation(fee_payment);
let token = Self::token_id_from_operation(delegate_change);

if fee_payment.account != delegate_change.account {
errors.push(PartialReason::FeePayerAndSourceMismatch);
}

// Validate the fee
let fee = Self::parse_amount_as_i64(fee_payment).map_err(|e| {
errors.push(e.clone());
MinaMeshError::OperationsNotValid(errors.clone())
})?;
if fee >= 0 {
errors.push(PartialReason::FeeNotNegative);
}

if let Some(metadata) = &delegate_change.metadata {
// Validate the delegate_change_target is present
if metadata.get("delegate_change_target").is_none() {
errors.push(PartialReason::InvalidMetadata(
"Missing delegate_change_target in delegate_change metadata".to_string(),
));
}
} else {
errors.push(PartialReason::InvalidMetadata(
"Missing delegate_change metadata with delegate_change_target".to_string(),
));
}

if !errors.is_empty() {
return Err(MinaMeshError::OperationsNotValid(errors));
}

Ok(PartialUserCommand {
kind: UserCommandType::Delegation,
fee_payer: Self::address_from_operation(fee_payment),
source: Self::address_from_operation(fee_payment),
receiver: Self::address_from_operation(delegate_change),
fee_token,
token,
fee,
amount: None,
valid_until,
memo,
})
}

fn find_operation(operations: &[Operation], op_type: OperationType) -> Result<&Operation, PartialReason> {
operations
.iter()
.find(|op| op.r#type == op_type.to_string())
.ok_or(PartialReason::CanNotFindKind(op_type.to_string()))
}

fn parse_amount_as_i64(operation: &Operation) -> Result<i64, PartialReason> {
operation
.amount
.as_ref()
.ok_or(PartialReason::AmountNotSome)
.and_then(|amount| amount.value.parse::<i64>().map_err(|_| PartialReason::AmountNotValid))
}

fn token_id_from_operation(operation: &Operation) -> String {
operation
.account
.as_ref()
.and_then(|account| account.metadata.as_ref())
.and_then(|meta| meta.get("token_id").and_then(|t| t.as_str()))
.unwrap_or(DEFAULT_TOKEN_ID)
.to_string()
}

fn address_from_operation(operation: &Operation) -> String {
if operation.r#type == DelegateChange.to_string() {
operation
.metadata
.as_ref()
.and_then(|meta| meta.get("delegate_change_target").and_then(|t| t.as_str()))
.unwrap_or_default()
.to_string()
} else {
operation.account.as_ref().map_or_else(String::new, |acc| acc.address.clone())
}
}
}
49 changes: 49 additions & 0 deletions src/base58.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use sha2::Digest;

use crate::MinaMeshError;

/// https://github.com/MinaProtocol/mina/blob/985eda49bdfabc046ef9001d3c406e688bc7ec45/src/lib/base58_check/base58_check.ml
///
/// Validates a base58-encoded string with checksum and optional version byte.
///
/// # Arguments
/// * `input` - The base58-encoded string to validate.
/// * `expected_version` - An optional expected version byte for validation.
///
/// # Returns
/// * `Ok(())` if the input is valid.
/// * `Err(MinaMeshError)` if the input is invalid.
pub fn validate_base58_with_checksum(input: &str, expected_version: Option<u8>) -> Result<(), MinaMeshError> {
// Decode the input using base58
let bytes = bs58::decode(input)
.with_alphabet(bs58::Alphabet::BITCOIN)
.into_vec()
.map_err(|_| MinaMeshError::MalformedPublicKey("Input not valid base58".to_string()))?;

// Check the length (must include at least version and checksum)
if bytes.len() < 5 {
return Err(MinaMeshError::MalformedPublicKey("Input too short".to_string()));
}

// Split into version, payload, and checksum
let (version, rest) = bytes.split_at(1);
let (payload, checksum) = rest.split_at(rest.len() - 4);

// Validate version byte if specified
if let Some(expected) = expected_version {
if version[0] != expected {
return Err(MinaMeshError::MalformedPublicKey(format!(
"Unexpected version byte: expected {}, got {}",
expected, version[0]
)));
}
}

// Recompute checksum
let computed_checksum = sha2::Sha256::digest(sha2::Sha256::digest([version, payload].concat()));
if &computed_checksum[.. 4] != checksum {
return Err(MinaMeshError::MalformedPublicKey("Checksum mismatch".to_string()));
}

Ok(())
}
Loading

0 comments on commit 0644535

Please sign in to comment.