Stake Priority Based Queuing

Stake Based Priority Queuing

In the below post I try and layout some ideas I had bouncing around in my head on preventing Spam and Denial of Service Attacks in a peer to peer (P2P) network - which is mainly achieved by introducing optional staking on an address or peer id.

Staking as a signal

The purpose of the staking contract is to enable node in a P2P network to apply rate based limiting, especially in the case of high congestion. To keep things simple a node should have two queuing buckets: a priority queue and a free tier queue e.g.

- Free Bucket (1Mbps)
    - Routes/Forwards traffic any in and outbound traffic.
- Priority Bucket (2Mbps)
    - Routes/Forwards traffic in and outbound traffic, weighted by stake.

These settings are obviously just recommended, and each node can pick it’s own settings. The benefit of the staking signal (and therefore the spam resistance) is obviously stronger based on how much of the node elects to each bucket. Having two buckets allows flexibility in terms of how alturistic a node wants to be.

The rest of the post will be discussing how the weighting works in the Priority Bucket.

The Priority Bucket

The priority bucket requires a message to be signed cryptographically by a proof that is stored on the staking contract. For a simple ECC proof one could easily sign it on a per-message basis; for more computationally expensive proofs one could sign based on a epoch time (every 5 minutes for example). To keep things simple, we can just assume address + ECC proof of address is used for the rest of the post.

The staking weight is calculated by the simple formula: final_weight(<address>) = balanceOf(<address>) / totalSupply. The staking contract starts with zero totalSupply; and the stake in the contract is purchased using a custom purchase function that ensures some unique attributes.

Once the node confirms a message/connection/circuit as a valid staked weight, the message/connection/circuit is placed and forwarded in the priority bucket accordingly.

Even though initially weight calculations are expensive; the subsequent caching of weights and or balances should make this rather trivial (if you require ~128 bytes per address, you could cache ~1 million values in 128 MB of RAM). To prevent caching attacks, one would probably just catch up on the latest contract state, before starting to forward or looking up proofs.

Per Address Purchase price

The per-address purchase price of credits; is limited by an exponential function I quite like 1/x^2. This means that purchasing additional routing/forwarding power becomes increasingly difficult and expensive.

balance_to_add(<address>, amount_to_purchase) = amount_to_purchase *(1/(1 + (amount_to_purchase + BalanceOf(<address>)^2))

Minimum Stake

Purchasing of stake in the Staking Contract requires a minimum purchase price as well. The stake of any participant can be withdrawn from the contract, but an nonredeemable deposit will always remain. For the sake of a reasonable example - you are required to purchase a minimum of 1 of stake and withdraw 0.5 of the stake.

Transfers

The bonus of making the Staking contract ERC20 compatible is that we can also transfer weight to other users up until the nonredeemable deposit, however transfers would have to be double taxed (with the existing per address formula and minimum requirements) to ensure transfers are not used as means to purchase too much forwarding power. This could also be simplified to just keep a running purchase balance per address, meaning even if transfers are cheap and are not taxed, additional purchases will be priced exponentially.

ERC20 compatibility also allows more free-market economics to come into play, by allowing participants to purchase and price credits directly in a DEX (yes I am thinking of Uniswap).

So how does this protect a node?

Because of the balanceOf(<address>) / totaSupply final weight calculation combined with a minimum stake and unredeemable deposit; an attacker would have to purchase an ever increasing amount to try and flood a P2P Network.

Example Staking Contract

priority_stake.vy

MINIMUM_STAKE: constant(uint256(wei)) = 1 * 10 ** 17
NONREFUNDABLE_DEPOSIT: constant(uint256) = 5 * 10 ** 16
    
# ... ERC20 stuff

@constant
@private
def get_amount_to_add(current_balance: uint256, amount: uint256(wei)) -> uint256(1/wei):
    return amount * (1 / (1 + (amount + current_balance) ** 2))

@public
@payable
def purchase_credits(node: address):
    assert msg.value > MINIMUM_STAKE
    assert msg.sender == node
    current_balance: uint256 = self.balanceOf[node]
    self.balanceOf[node] += as_unitless_number(
        self.get_amount_to_add(current_balance, msg.value)
    )

@public
def witdraw_credits(node: address, amount: uint256):
    assert msg.sender == node
    current_balance: uint256 = self.balanceOf[node]
    assert current_balance > NONREFUNDABLE_DEPOSIT
    send(node, current_balance - NONREFUNDABLE_DEPOSIT)

Variations on the the above

  • Instead of having an per-address limiting purchasing formula, one uses a curve based on the total supply; I am not too keen on this as this benefits early adoptors way too much.
  • Calculate the weight using weight(<src_address>, <dst_address>) = (balanceOf(<dst_address>) * balanceOf(<src_address>)) / totalSupply. Which brings some interesting things into play. If you wanted a message to a have very high guarantees of reaching a participant you could transfer some credits into their account (if you know what proof they used of course). And if someone was under attack, they could just keep transferring away their stake and get payed to be spammed :slight_smile:
  • If you did the rate limiting based on the just on the proof - and just account for the proof linked packets that you forward, one could add an additional layer of anonymity.

Discussion

  • One big part that makes this trickier, is how much a node reveals of it’s identity by the proof that gets used in the contract.
  • balanceOf(<address>) / totalSupply might not de-escalate fast enough. Which means you could tie all addresses into one dynamic purchase price dependant on totalSupply.
3 Likes

Would something like semaphore make sense to use here?

Well this is a simpler than Semaphore; and doesn’t dictate which which cryptographic proof you want to sign your messages with. Semaphore also uses slashing which this doesn’t do - this opts to instead just signal participation; which I find quite cool - if you signal participation around a certain contract; others will join and you get network effect (if Status signalled a contract we’d be set; but still allows opting to build other contracts + networks).

The anonymity here is trickier; but if you assume that you can produce a address tied to your node that has mixed funds you are good to go. I really like just using ECC / eth addresses because the proofs are so much faster.

If you do go the zero-knowledge way; one could probably write the staking contract in such a way that one can sign a specific amount credits? And then just attach the proof of credits without revealing your identity? Which then becomes a lot like semaphore; but without the slashing?

Another realisation:
You can do the staking separate from the actual <src; dst> of the peer node which could make it way more anonymous. If you did the rate limiting based on the just on the proof - and just account for the packets that you forward.

2 Likes

Another cool realisation I added to the list:

You could take the non-redeemable deposit and return it to the owner/network of the organisation for funding of their servers & nodes - this would be a very traditional crowd funding model and would have no obvious way to verify that the funds would be used for their purpose; except that one contract = one network and you vote with your funds where you want to pay - as well which network you want to participate in. Extending to a Rent model - is also easy here; by just using a factory pattern on the contract with a given time period (monthly/yearly/weekly).

1 Like

This is great, thanks! I think it’s likely we’ll investigate this once waku/1 with improved bandwidth is done. This is a good candidate for v2 IMO. Adding to roadmap issue here https://github.com/vacp2p/pm/issues/5