Skip to content

Commit

Permalink
Add tx validation (#4)
Browse files Browse the repository at this point in the history
Transaction validity is critical to the chain consistency.

The specification of the routines is defined in
https://github.com/FuelLabs/fuel-specs/blob/master/specs/protocol/tx_validity.md

Some of these specs may intersect with VM logic; these intersections are
outside the scope of this lib and should be implemented directly in the
VM.

Resolves #3
  • Loading branch information
vlopes11 authored May 18, 2021
1 parent c4bb624 commit e2efb2f
Show file tree
Hide file tree
Showing 10 changed files with 953 additions and 1 deletion.
33 changes: 33 additions & 0 deletions fuel-tx/src/consts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/// Maximum contract size, in bytes.
pub const CONTRACT_MAX_SIZE: u64 = 16 * 1024;

/// Maximum number of inputs.
pub const MAX_INPUTS: u8 = 8;

/// Maximum number of outputs.
pub const MAX_OUTPUTS: u8 = 8;

/// Maximum number of witnesses.
pub const MAX_WITNESSES: u8 = 16;

/// Maximum gas per transaction.
pub const MAX_GAS_PER_TX: u64 = 1000000;

// TODO set max script length const
/// Maximum length of script, in instructions.
pub const MAX_SCRIPT_LENGTH: u64 = 1024 * 1024 * 1024;

// TODO set max script length const
/// Maximum length of script data, in bytes.
pub const MAX_SCRIPT_DATA_LENGTH: u64 = 1024 * 1024 * 1024;

/// Maximum number of static contracts.
pub const MAX_STATIC_CONTRACTS: u64 = 255;

// TODO set max predicate length value
/// Maximum length of predicate, in instructions.
pub const MAX_PREDICATE_LENGTH: u64 = 1024 * 1024;

// TODO set max predicate data length value
/// Maximum length of predicate data, in bytes.
pub const MAX_PREDICATE_DATA_LENGTH: u64 = 1024 * 1024;
4 changes: 3 additions & 1 deletion fuel-tx/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#![feature(arbitrary_enum_discriminant)]
#![feature(is_sorted)]

// TODO Add docs

mod transaction;

pub mod bytes;
pub mod consts;

pub use transaction::{Color, Id, Input, Output, Root, Transaction, Witness};
pub use transaction::{Color, Id, Input, Output, Root, Transaction, ValidationError, Witness};
2 changes: 2 additions & 0 deletions fuel-tx/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ use std::io::Write;
use std::{io, mem};

mod types;
mod validation;

pub use types::{Color, Id, Input, Output, Root, Witness};
pub use validation::ValidationError;

const WORD_SIZE: usize = mem::size_of::<Word>();
const ID_SIZE: usize = mem::size_of::<Id>();
Expand Down
203 changes: 203 additions & 0 deletions fuel-tx/src/transaction/validation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
use super::{Color, Input, Output, Transaction, Witness};
use crate::consts::*;

use fuel_asm::Word;

use std::mem;

mod error;

pub use error::ValidationError;

const COLOR_SIZE: usize = mem::size_of::<Color>();

impl Input {
pub fn validate(&self, index: usize, outputs: &[Output], witnesses: &[Witness]) -> Result<(), ValidationError> {
match self {
Self::Coin { predicate, .. } if predicate.len() > MAX_PREDICATE_LENGTH as usize => {
Err(ValidationError::InputCoinPredicateLength { index })
}

Self::Coin { predicate_data, .. } if predicate_data.len() > MAX_PREDICATE_DATA_LENGTH as usize => {
Err(ValidationError::InputCoinPredicateDataLength { index })
}

Self::Coin { witness_index, .. } if *witness_index as usize >= witnesses.len() => {
Err(ValidationError::InputCoinWitnessIndexBounds { index })
}

// ∀ inputContract ∃! outputContract : outputContract.inputIndex = inputContract.index
Self::Contract { .. }
if 1 != outputs
.iter()
.filter_map(|output| match output {
Output::Contract { input_index, .. } if *input_index as usize == index => Some(()),
_ => None,
})
.count() =>
{
Err(ValidationError::InputContractAssociatedOutputContract { index })
}

// TODO If h is the block height the UTXO being spent was created, transaction is
// invalid if `blockheight() < h + maturity`.
_ => Ok(()),
}
}
}

impl Output {
pub fn validate(&self, index: usize, inputs: &[Input]) -> Result<(), ValidationError> {
match self {
Self::Contract { input_index, .. } => match inputs.get(*input_index as usize) {
Some(Input::Contract { .. }) => Ok(()),
_ => Err(ValidationError::OutputContractInputIndex { index }),
},

_ => Ok(()),
}
}
}

impl Transaction {
pub fn validate(&self, block_height: Word) -> Result<(), ValidationError> {
if self.gas_price() > MAX_GAS_PER_TX {
Err(ValidationError::TransactionGasLimit)?
}

if block_height < self.maturity() as Word {
Err(ValidationError::TransactionMaturity)?;
}

if self.inputs().len() > MAX_INPUTS as usize {
Err(ValidationError::TransactionInputsMax)?
}

if self.outputs().len() > MAX_OUTPUTS as usize {
Err(ValidationError::TransactionOutputsMax)?
}

if self.witnesses().len() > MAX_WITNESSES as usize {
Err(ValidationError::TransactionWitnessesMax)?
}

let input_colors: Vec<&Color> = self.input_colors().collect();
for input_color in input_colors.as_slice() {
if self
.outputs()
.iter()
.filter_map(|output| match output {
Output::Change { color, .. } if color != &Color::default() && input_color == &color => Some(()),
_ => None,
})
.count()
> 1
{
Err(ValidationError::TransactionOutputChangeColorDuplicated)?
}
}

for (index, input) in self.inputs().iter().enumerate() {
input.validate(index, self.outputs(), self.witnesses())?;
}

for (index, output) in self.outputs().iter().enumerate() {
output.validate(index, self.inputs())?;
if let Output::Change { color, .. } = output {
if !input_colors.iter().any(|input_color| input_color == &color) {
Err(ValidationError::TransactionOutputChangeColorNotFound)?
}
}
}

match self {
Self::Script {
outputs,
script,
script_data,
..
} => {
if script.len() > MAX_SCRIPT_LENGTH as usize {
Err(ValidationError::TransactionScriptLength)?;
}

if script_data.len() > MAX_SCRIPT_DATA_LENGTH as usize {
Err(ValidationError::TransactionScriptDataLength)?;
}

outputs
.iter()
.enumerate()
.try_for_each(|(index, output)| match output {
Output::ContractCreated { .. } => {
Err(ValidationError::TransactionScriptOutputContractCreated { index })
}
_ => Ok(()),
})?;

Ok(())
}

Self::Create {
inputs,
outputs,
witnesses,
bytecode_witness_index,
static_contracts,
..
} => {
match witnesses.get(*bytecode_witness_index as usize) {
Some(witness) if witness.as_ref().len() as u64 * 4 > CONTRACT_MAX_SIZE => {
Err(ValidationError::TransactionCreateBytecodeLen)?
}
None => Err(ValidationError::TransactionCreateBytecodeWitnessIndex)?,
_ => (),
}

if static_contracts.len() > MAX_STATIC_CONTRACTS as usize {
Err(ValidationError::TransactionCreateStaticContractsMax)?;
}

if !static_contracts.as_slice().is_sorted() {
Err(ValidationError::TransactionCreateStaticContractsOrder)?;
}

// TODO Any contract with ID in staticContracts is not in the state
// TODO The computed contract ID (see below) is not equal to the contractID of
// the one OutputType.ContractCreated output

for (index, input) in inputs.iter().enumerate() {
if let Input::Contract { .. } = input {
Err(ValidationError::TransactionCreateInputContract { index })?
}
}

let mut change_color_zero = false;
let mut contract_created = false;
for (index, output) in outputs.iter().enumerate() {
match output {
Output::Contract { .. } => Err(ValidationError::TransactionCreateOutputContract { index })?,
Output::Variable { .. } => Err(ValidationError::TransactionCreateOutputVariable { index })?,

Output::Change { color, .. } if color == &[0u8; COLOR_SIZE] && change_color_zero => {
Err(ValidationError::TransactionCreateOutputChangeColorZero { index })?
}
Output::Change { color, .. } if color == &[0u8; COLOR_SIZE] => change_color_zero = true,
Output::Change { .. } => {
Err(ValidationError::TransactionCreateOutputChangeColorNonZero { index })?
}

Output::ContractCreated { .. } if contract_created => {
Err(ValidationError::TransactionCreateOutputContractCreatedMultiple { index })?
}
Output::ContractCreated { .. } => contract_created = true,

_ => (),
}
}

Ok(())
}
}
}
}
49 changes: 49 additions & 0 deletions fuel-tx/src/transaction/validation/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use std::{error, fmt, io};

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ValidationError {
InputCoinPredicateLength { index: usize },
InputCoinPredicateDataLength { index: usize },
InputCoinWitnessIndexBounds { index: usize },
InputContractAssociatedOutputContract { index: usize },
OutputContractInputIndex { index: usize },
TransactionCreateInputContract { index: usize },
TransactionCreateOutputContract { index: usize },
TransactionCreateOutputVariable { index: usize },
TransactionCreateOutputChangeColorZero { index: usize },
TransactionCreateOutputChangeColorNonZero { index: usize },
TransactionCreateOutputContractCreatedMultiple { index: usize },
TransactionCreateBytecodeLen,
TransactionCreateBytecodeWitnessIndex,
TransactionCreateStaticContractsMax,
TransactionCreateStaticContractsOrder,
TransactionScriptLength,
TransactionScriptDataLength,
TransactionScriptOutputContractCreated { index: usize },
TransactionGasLimit,
TransactionMaturity,
TransactionInputsMax,
TransactionOutputsMax,
TransactionWitnessesMax,
TransactionOutputChangeColorDuplicated,
TransactionOutputChangeColorNotFound,
}

impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// TODO better describe the error variants
write!(f, "{:?}", self)
}
}

impl error::Error for ValidationError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
None
}
}

impl From<ValidationError> for io::Error {
fn from(v: ValidationError) -> io::Error {
io::Error::new(io::ErrorKind::Other, v)
}
}
2 changes: 2 additions & 0 deletions fuel-tx/tests/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use fuel_tx::*;
use std::fmt;
use std::io::{self, Read, Write};

mod valid;

pub fn assert_encoding_correct<T>(data: &[T])
where
T: Read + Write + fmt::Debug + Clone + PartialEq,
Expand Down
Loading

0 comments on commit e2efb2f

Please sign in to comment.