Implement a fee on every input to prevent prior described economic attacks

Completes #297.
This commit is contained in:
Luke Parker
2023-10-22 21:31:13 -04:00
parent f561fa9ba1
commit fd1826cca9
6 changed files with 89 additions and 32 deletions

View File

@@ -5,7 +5,7 @@ effectively be guaranteed to terminate with a safe end state. This document
attempts to detail such requirements, and the implementations in Serai resolving attempts to detail such requirements, and the implementations in Serai resolving
them. them.
### Fees From Effecting Transactions Out ## Fees From Effecting Transactions Out
When `sriXYZ` is burnt, Serai is expected to create an output for `XYZ` as When `sriXYZ` is burnt, Serai is expected to create an output for `XYZ` as
instructed. The transaction containing this output will presumably have some fee instructed. The transaction containing this output will presumably have some fee
@@ -25,7 +25,7 @@ before the burn is included on-chain. Not only would this require more data be
published to Serai (widening data pipeline requirements), it'd prevent any published to Serai (widening data pipeline requirements), it'd prevent any
RBF-based solutions to dynamic fee markets causing transactions to get stuck. RBF-based solutions to dynamic fee markets causing transactions to get stuck.
### Output Frequency ## Output Frequency
Outputs can be created on an external network at rate Outputs can be created on an external network at rate
`max_outputs_per_tx / external_tick_rate`, where `external_tick_rate` is the `max_outputs_per_tx / external_tick_rate`, where `external_tick_rate` is the
@@ -86,7 +86,7 @@ fulfill an output, increasing the fee amortized over the output and its
siblings, this fee scales linearly with the logarithmically scaling tree depth. siblings, this fee scales linearly with the logarithmically scaling tree depth.
This is considered acceptable. This is considered acceptable.
### Input Availability ## Input Availability
The following section refers to spending an output, and then spending it again. The following section refers to spending an output, and then spending it again.
Spending it again, which is impossible under the UTXO model, refers to spending Spending it again, which is impossible under the UTXO model, refers to spending
@@ -118,7 +118,7 @@ notably large burn, then the entire global queue will be consumed as full input
availability means the ability to satisfy all potential burns in a solvent availability means the ability to satisfy all potential burns in a solvent
system. system.
### Fees Incurred During Operations ## Fees Incurred During Operations
While fees incurred when satisfying burn were covered above, with documentation While fees incurred when satisfying burn were covered above, with documentation
on how solvency is maintained, two other operating costs exists. on how solvency is maintained, two other operating costs exists.
@@ -159,6 +159,8 @@ created transaction the running operating costs. When a created transaction has
payments out, all of the operating costs incurred so far, which have yet to be payments out, all of the operating costs incurred so far, which have yet to be
amortized, are immediately and fully amortized. amortized, are immediately and fully amortized.
## Attacks by a Malicious Miner
There is the concern that a significant amount of outputs could be created, There is the concern that a significant amount of outputs could be created,
which when merged as inputs, create a significant amount of operating costs. which when merged as inputs, create a significant amount of operating costs.
This would then be forced onto random users who burn `sriXYZ` soon after, while This would then be forced onto random users who burn `sriXYZ` soon after, while
@@ -166,32 +168,55 @@ the party who caused the operating costs would then be able to burn their own
`sriXYZ` without notable fees. `sriXYZ` without notable fees.
To describe this attack in its optimal form, assume a sole malicious block To describe this attack in its optimal form, assume a sole malicious block
producer for an external network where `max_inputs_per_tx` is 16. The malicious producer for an external network. The malicious miner adds an output to Serai,
miner adds 256 outputs to Serai, not paying any fees as the block producer. not paying any fees as the block producer. This single output alone may trigger
Serai must create 16 transactions to produce a set of 16 inputs, paying for 16 an aggregation transaction. Serai would pay for the transaction fee, the fee
transaction fees in the process (the fees of which go to the malicious miner). going to the malicious miner.
When Serai users burn `sriXYZ`, they are hit with the 16 transaction fees plus When Serai users burn `sriXYZ`, they are hit with the aggregation transaction's
the normally amortized fee. Then, the malicious miner burns their `sriXYZ`, fee plus the normally amortized fee. Then, the malicious miner burns their
having the fee they capture be amortized over their output. In this process, `sriXYZ`, having the fee they capture be amortized over their output. In this
they remain net except for the 16 transaction fees they gain from other users, process, they remain net except for the increased transaction fees they gain
which they profit. from other users, which they profit.
A miner only has to have 7% of the external network's hash power to execute this
attack profitably. By only minting `sriXYZ` during their blocks, they pay no
fees. Then, _a miner_, which has a 7% chance of being themselves, collects the
16 transaction fees. Finally, they burn, with a 7% chance of collecting their
own fee, or a 93% chance of losing a single transaction fee.
16 attempts, costing 16 transaction fees if they always lose their single
transaction fee, will cause a slight edge they gain the 16 transaction fees at
least once, offsetting their costs.
To limit this attack vector, a flat fee of To limit this attack vector, a flat fee of
`2 * (the estimation of an input-merging transaction fee) / max_inputs_per_tx` `2 * (the estimation of a 2-input-merging transaction fee)` is applied to each
is applied to each input. This means, assuming an inability to manipulate input. This means, assuming an inability to manipulate Serai's fee estimations,
Serai's fee estimations, creating 16 outputs to force a merge transaction (and creating an output to force a merge transaction (and the associated fee) costs
the associated fee) costs the attacker twice as much as the associated fee. the attacker twice as much as the associated fee.
A 2-input TX's fee is used as aggregating multiple inputs at once actually
yields in Serai's favor so long as the per-input fee exceeds the cost of the
per-input addition to the TX. Since the per-input fee is the cost of an entire
TX, this property is true.
### Profitability Without the Flat Fee With a Minority of Hash Power
Ignoring the above flat fee, a malicious miner could use aggregating multiple
inputs to achieve profit with a minority of hash power. The following is how a
miner with 7% of the external network's hash power could execute this attack
profitably over a network with a `max_inputs_per_tx` value of 16:
1) Mint `sriXYZ` with 256 outputs during their own blocks. This incurs no fees
and would force 16 aggregation transactions to be created.
2) _A miner_, which has a 7% chance of being the malicious miner, collects the
16 transaction fees.
3) The malicious miner burns their sriXYZ, with a 7% chance of collecting their
own fee or a 93% chance of losing a single transaction fee.
16 attempts would cost 16 transaction fees if they always lose their single
transaction fee. Gaining the 16 transaction fees once, offsetting costs, is
expected to happen with just 6.25% of the hash power. Since the malicious miner
has 7%, they're statistically likely to recoup their costs and eventually turn
a profit.
With a flat fee of at least the cost to aggregate a single input in a full
aggregation transaction, this attack falls apart. Serai's flat fee is the higher
cost of the fee to aggregate two inputs in an aggregation transaction.
### Solvency Without the Flat Fee
Even without the above flat fee, Serai remains solvent. With the above flat fee, Even without the above flat fee, Serai remains solvent. With the above flat fee,
malicious miners on external networks can only steal from other users if they malicious miners on external networks can only steal from other users if they

View File

@@ -57,9 +57,10 @@ fn instruction_from_output<N: Network>(output: &N::Output) -> Option<InInstructi
let Ok(shorthand) = Shorthand::decode(&mut data) else { None? }; let Ok(shorthand) = Shorthand::decode(&mut data) else { None? };
let Ok(instruction) = RefundableInInstruction::try_from(shorthand) else { None? }; let Ok(instruction) = RefundableInInstruction::try_from(shorthand) else { None? };
let balance = output.balance(); let mut balance = output.balance();
// TODO: Decrease amount by // Deduct twice the cost to aggregate to prevent economic attacks by malicious miners against
// `2 * (the estimation of an input-merging transaction fee) / max_inputs_per_tx` // other users
balance.amount.0 -= 2 * N::COST_TO_AGGREGATE;
// TODO2: Set instruction.origin if not set (and handle refunds in general) // TODO2: Set instruction.origin if not set (and handle refunds in general)
Some(InInstructionWithBalance { instruction: instruction.instruction, balance }) Some(InInstructionWithBalance { instruction: instruction.instruction, balance })

View File

@@ -451,6 +451,14 @@ impl Network for Bitcoin {
*/ */
const DUST: u64 = 10_000; const DUST: u64 = 10_000;
// 2 inputs should be 2 * 230 = 460 weight units
// The output should be ~36 bytes, or 144 weight units
// The overhead should be ~20 bytes at most, or 80 weight units
// 684 weight units, 171 vbytes, round up to 200
// 200 vbytes at 1 sat/weight (our current minumum fee, 4 sat/vbyte) = 800 sat fee for the
// aggregation TX
const COST_TO_AGGREGATE: u64 = 800;
// Bitcoin has a max weight of 400,000 (MAX_STANDARD_TX_WEIGHT) // Bitcoin has a max weight of 400,000 (MAX_STANDARD_TX_WEIGHT)
// A non-SegWit TX will have 4 weight units per byte, leaving a max size of 100,000 bytes // A non-SegWit TX will have 4 weight units per byte, leaving a max size of 100,000 bytes
// While our inputs are entirely SegWit, such fine tuning is not necessary and could create // While our inputs are entirely SegWit, such fine tuning is not necessary and could create

View File

@@ -278,6 +278,9 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
/// magnitude). /// magnitude).
const DUST: u64; const DUST: u64;
/// The cost to perform input aggregation with a 2-input 1-output TX.
const COST_TO_AGGREGATE: u64;
/// Tweak keys for this network. /// Tweak keys for this network.
fn tweak_keys(key: &mut ThresholdKeys<Self::Curve>); fn tweak_keys(key: &mut ThresholdKeys<Self::Curve>);

View File

@@ -397,6 +397,9 @@ impl Network for Monero {
// TODO: Set a sane dust // TODO: Set a sane dust
const DUST: u64 = 10000000000; const DUST: u64 = 10000000000;
// TODO
const COST_TO_AGGREGATE: u64 = 0;
// Monero doesn't require/benefit from tweaking // Monero doesn't require/benefit from tweaking
fn tweak_keys(_: &mut ThresholdKeys<Self::Curve>) {} fn tweak_keys(_: &mut ThresholdKeys<Self::Curve>) {}

View File

@@ -8,12 +8,16 @@ use dkg::{Participant, tests::clone_without};
use messages::{coordinator::PlanMeta, sign::SignId, SubstrateContext}; use messages::{coordinator::PlanMeta, sign::SignId, SubstrateContext};
use serai_client::{ use serai_client::{
primitives::{BlockHash, crypto::RuntimePublic, PublicKey, SeraiAddress, NetworkId}, primitives::{
BlockHash, Amount, Balance, crypto::RuntimePublic, PublicKey, SeraiAddress, NetworkId,
},
in_instructions::primitives::{ in_instructions::primitives::{
InInstruction, InInstructionWithBalance, Batch, SignedBatch, batch_message, InInstruction, InInstructionWithBalance, Batch, SignedBatch, batch_message,
}, },
}; };
use processor::networks::{Network, Bitcoin, Monero};
use crate::{*, tests::*}; use crate::{*, tests::*};
pub(crate) async fn recv_batch_preprocesses( pub(crate) async fn recv_batch_preprocesses(
@@ -247,7 +251,20 @@ fn batch_test() {
id: i, id: i,
block: BlockHash(block_with_tx.unwrap()), block: BlockHash(block_with_tx.unwrap()),
instructions: if let Some(instruction) = instruction { instructions: if let Some(instruction) = instruction {
vec![InInstructionWithBalance { instruction, balance: balance_sent }] vec![InInstructionWithBalance {
instruction,
balance: Balance {
coin: balance_sent.coin,
amount: Amount(
balance_sent.amount.0 -
(2 * if network == NetworkId::Bitcoin {
Bitcoin::COST_TO_AGGREGATE
} else {
Monero::COST_TO_AGGREGATE
}),
),
},
}]
} else { } else {
// This shouldn't have an instruction as we didn't add any data into the TX we sent // This shouldn't have an instruction as we didn't add any data into the TX we sent
// Empty batches remain valuable as they let us achieve consensus on the block and spend // Empty batches remain valuable as they let us achieve consensus on the block and spend