Skip to content

Commit

Permalink
[feat] Prevent double spending: UTXOs created and spent in the same b…
Browse files Browse the repository at this point in the history
…lock (#173)
  • Loading branch information
TAdev0 authored Sep 15, 2024
1 parent 89c8055 commit a79c576
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 50 deletions.
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
scarb 2.8.0
scarb 2.8.2
28 changes: 19 additions & 9 deletions packages/client/src/main.cairo
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
use consensus::types::block::Block;
use consensus::types::chain_state::{ChainState, BlockValidator};
use consensus::types::state::State;
use consensus::types::chain_state::BlockValidator;
use consensus::types::utxo_set::UtxoSet;

/// Raito program arguments.
#[derive(Serde)]
struct Args {
/// Current (initial) chain state
chain_state: ChainState,
/// Current (initial) state
state: State,
/// Batch of blocks that have to be applied to the current chain state
blocks: Array<Block>,
}

/// Raito program entrypoint.
///
/// Receives current chain state and pending blocks,
/// Receives current state (chain state + utreexo state) and pending blocks,
/// then validates and applies them one by one.
/// Returns new chain state in case of succes, otherwise raises an error.
fn main(mut arguments: Span<felt252>) -> ChainState {
let Args { mut chain_state, blocks, } = Serde::deserialize(ref arguments)
/// Returns new state in case of succes, otherwise raises an error.
fn main(mut arguments: Span<felt252>) -> State {
let Args { mut state, blocks, } = Serde::deserialize(ref arguments)
.expect('Failed to deserialize');

let mut utxo_set = UtxoSet { utreexo_state: state.utreexo_state, cache: Default::default(), };

for block in blocks {
chain_state = chain_state.validate_and_apply(block).expect('Validation failed');
state
.chain_state = state
.chain_state
.validate_and_apply(block, ref utxo_set)
.expect('Validation failed');
};
chain_state

state.utreexo_state = utxo_set.utreexo_state;
state
}
23 changes: 16 additions & 7 deletions packages/client/src/test.cairo
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use consensus::types::block::Block;
use consensus::types::chain_state::{ChainState, BlockValidator};
use consensus::types::chain_state::{ChainState, BlockValidatorImpl};
use consensus::types::state::{State};
use consensus::types::utxo_set::UtxoSet;
use core::testing::get_available_gas;

/// Integration testing program arguments.
Expand All @@ -22,13 +24,20 @@ fn test(mut arguments: Span<felt252>) {
let Args { mut chain_state, blocks, expected_chain_state } = Serde::deserialize(ref arguments)
.expect('Failed to deserialize');

// Temporary solution while script doesn't handle utreexo.
// Allows to test one isolated block, or a batch of blocks starting from genesis.
let mut state: State = State { chain_state: chain_state, utreexo_state: Default::default(), };
let mut utxo_set: UtxoSet = UtxoSet {
utreexo_state: state.utreexo_state, cache: Default::default()
};

let mut gas_before = get_available_gas();

for block in blocks {
let height = chain_state.block_height + 1;
match chain_state.validate_and_apply(block) {
let height = state.chain_state.block_height + 1;
match state.chain_state.validate_and_apply(block, ref utxo_set) {
Result::Ok(new_chain_state) => {
chain_state = new_chain_state;
state.chain_state = new_chain_state;
let gas_after = get_available_gas();
println!("OK: block={} gas_spent={}", height, gas_before - gas_after);
gas_before = gas_after;
Expand All @@ -43,12 +52,12 @@ fn test(mut arguments: Span<felt252>) {
}
};

if chain_state != expected_chain_state {
if state.chain_state != expected_chain_state {
println!(
"FAIL: block={} error='expected state {:?}, actual {:?}'",
chain_state.block_height,
state.chain_state.block_height,
expected_chain_state,
chain_state
state.chain_state
);
panic!();
}
Expand Down
1 change: 1 addition & 0 deletions packages/consensus/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ pub mod types {
pub mod block;
pub mod transaction;
pub mod utxo_set;
pub mod state;
}
11 changes: 7 additions & 4 deletions packages/consensus/src/types/chain_state.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
//! Chain state alone is not enough to do full block validation, however
//! it is sufficient to validate block headers.

use utils::hash::Digest;
use core::fmt::{Display, Formatter, Error};
use crate::validation::{
difficulty::{validate_bits, adjust_difficulty}, coinbase::validate_coinbase,
timestamp::{validate_timestamp, next_prev_timestamps},
work::{validate_proof_of_work, compute_total_work}, block::{compute_and_validate_tx_data},
};
use super::block::{BlockHash, Block, TransactionData};
use core::fmt::{Display, Formatter, Error};
use super::utxo_set::UtxoSet;
use utils::hash::Digest;

/// Represents the state of the blockchain.
#[derive(Drop, Copy, Debug, PartialEq, Serde)]
Expand Down Expand Up @@ -55,7 +56,9 @@ impl ChainStateDefault of Default<ChainState> {
/// Full block validator (w/o bitcoin script checks and utxo inclusion verification for now).
#[generate_trait]
pub impl BlockValidatorImpl of BlockValidator {
fn validate_and_apply(self: ChainState, block: Block) -> Result<ChainState, ByteArray> {
fn validate_and_apply(
self: ChainState, block: Block, ref utxo_set: UtxoSet
) -> Result<ChainState, ByteArray> {
let block_height = self.block_height + 1;

validate_timestamp(self.prev_timestamps, block.header.time)?;
Expand All @@ -66,7 +69,7 @@ pub impl BlockValidatorImpl of BlockValidator {
TransactionData::MerkleRoot(root) => root,
TransactionData::Transactions(txs) => {
let (total_fees, txid_root, wtxid_root) = compute_and_validate_tx_data(
txs, block_height, block.header.time
txs, block_height, block.header.time, ref utxo_set
)?;
validate_coinbase(txs[0], total_fees, block_height, wtxid_root)?;
txid_root
Expand Down
10 changes: 10 additions & 0 deletions packages/consensus/src/types/state.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! State is a top level struct containing the chain state and the utxo set

use crate::types::utreexo::{UtreexoState, UtreexoStateDefault};
use super::chain_state::ChainState;

#[derive(Default, Drop, Copy, Debug, Serde)]
pub struct State {
pub chain_state: ChainState,
pub utreexo_state: UtreexoState
}
20 changes: 14 additions & 6 deletions packages/consensus/src/types/utxo_set.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@
//! Utreexo accumulator or local cache.

use core::dict::Felt252Dict;
use core::hash::{HashStateTrait, HashStateExTrait};
use core::poseidon::PoseidonTrait;
use super::utreexo::UtreexoState;
use super::transaction::OutPoint;

#[derive(Default)]
#[derive(Default, Destruct)]
pub struct UtxoSet {
/// Utreexo state.
utreexo_state: UtreexoState,
pub utreexo_state: UtreexoState,
/// Hashes of UTXOs created within the current block(s).
/// Note that to preserve the ordering, cache has to be updated right after a
/// particular output is created or spent.
cache: Felt252Dict<()>,
pub cache: Felt252Dict<bool>,
}

#[generate_trait]
Expand All @@ -30,15 +32,21 @@ pub impl UtxoSetImpl of UtxoSetTrait {
}

fn add(ref self: UtxoSet, output: OutPoint) {
if output.data.cached { // TODO: add to the local block cache
if output.data.cached {
let outpoint_hash = PoseidonTrait::new().update_with(output).finalize();
self.cache.insert(outpoint_hash, true);
} else { // TODO: update utreexo roots
}
}

fn delete(ref self: UtxoSet, output: @OutPoint) {
if *output.data.cached { // TODO: remove from cache (+ verify inclusion)
if *output.data.cached {
let outpoint_hash = PoseidonTrait::new().update_with(*output).finalize();
// Extra check that can be removed later.
assert(self.cache.get(outpoint_hash), 'output is not cached');
self.cache.insert(outpoint_hash, false);
} else { // TODO: update utreexo roots (+ verify inclusion)
// If batched proofs are used then do nothing
// If batched proofs are used then do nothing.
}
}
}
8 changes: 5 additions & 3 deletions packages/consensus/src/validation/block.cairo
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Block validation helpers.
use crate::types::transaction::{Transaction};
use crate::types::utxo_set::UtxoSet;
use crate::types::transaction::Transaction;
use crate::codec::{Encode, TransactionCodec};
use utils::{hash::Digest, merkle_tree::merkle_root, sha256::double_sha256_byte_array};
use super::transaction::validate_transaction;
Expand Down Expand Up @@ -27,7 +28,7 @@ pub fn validate_block_weight(weight: usize) -> Result<(), ByteArray> {
/// - wTXID commitment (only for blocks after Segwit upgrade, otherwise return zero hash)
/// - Block weight
pub fn compute_and_validate_tx_data(
txs: Span<Transaction>, block_height: u32, block_time: u32
txs: Span<Transaction>, block_height: u32, block_time: u32, ref utxo_set: UtxoSet
) -> Result<(u64, Digest, Digest), ByteArray> {
let mut txids: Array<Digest> = array![];
let mut wtxids: Array<Digest> = array![];
Expand Down Expand Up @@ -66,12 +67,13 @@ pub fn compute_and_validate_tx_data(

// skipping the coinbase transaction
if (i != 0) {
let fee = match validate_transaction(tx, block_height, block_time) {
let fee = match validate_transaction(tx, block_height, block_time, txid, ref utxo_set) {
Result::Ok(fee) => fee,
Result::Err(err) => { break Result::Err(err); }
};
total_fee += fee;
}

i += 1;
};
validate_transactions?;
Expand Down
Loading

0 comments on commit a79c576

Please sign in to comment.