mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Implement a fee on every input to prevent prior described economic attacks
Completes #297.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>);
|
||||||
|
|
||||||
|
|||||||
@@ -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>) {}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user