Vega does not charge fees on transactions. Fees are only charged on trades that execute. The main reason for not charging transaction fees on transactions like limit order submissions or governance votes is that this is an activity that should be encouraged (limit orders provide price information and liquidity, governance votes are essential for smooth running of the system). Because of this Vega has a novel client-side-proof-of-work mechanism to prevent transaction spam.
spam.pow.numberOfPastBlocks = 100 (range: 10-500)
spam.pow.difficulty = 15 <should correspond to ca. 1 seconds on a normal PC> (range:0-50)
spam.pow.hashFunction = sha3
spam.pow.numberOfTxPerBlock = 2 (range: 1-1000)
spam.pow.increaseDifficulty = 0 (range: 0/1)
If there is a governance vote on parameter changes taking effect at block height h, then the parameter is valid for all PoWs that are tied to a block of height h or later; that is, old pre-computations remain valid, and only new ones need to respect the new parameters. If spam.pow.numberOfPastBlocks
is changed at height h, the parameter is enforced starting at block h+spam.pow.numberOfPastBlocks
(being the new value).
There is a (theoretical) possibility that a parameter is changed repeatedly so fast that the new parameter is not enforced before it is changed again. To avoid programming complexity, the intermediate parameter is ignored, i.e., we keep the current parameter until the new one is valid.
Example: Suppose, we have numberOfPastBlocks
= 100, and difficulty = 15
If we change difficulty to 20 in block height 20 000, then all transactions tied to block 20 000 or later need to solve difficulty 20
However, if we then change the difficulty to 25 at block height 20 005, and the current block height is 19 990, then we keep difficulty 20 for all transactions tied to block 20 004 or earlier, and require 25 for block 20 005 or later.
Note: The latter is done so we don't need to do complex implementations for special cases that will never occur in reality; we need to store the current parameter, and a block height when it switches to a new one, and no intermediates. A way to avoid that would be to make parameter changes valid immediately; this however would cause special cases if users need to throw away transactions and recompute the PoW, which this approach avoids.
To authorise a transaction, it needs to be tied to a past block using a proof of work.
To this end, the hash of the block and a transaction identifier are fed into a hash function together with a padding; the proof of work is to find a padding that results in a hash that starts with spam.pow.difficulty
zeroes (when represented in bytes).
Thus, the flow is as follows:
- The user generates a unique transaction identifier
tid
- The user downloads the hash
H(b)
of the latest block it has seen (or uses any other block hash withinspam.pow.numberOfPastBlocks
of the block to-be-produced), and brute forces values ofx
such thathash("Vega_SPAM_PoW", H(b), tid, x)
as bytes starts withspam.pow.difficulty
zeros. - The user must monitor how many transactions they have sent with PoW tied to a given block.
- If this number is less than or equal to
spam.pow.numberOfTxPerBlock
the hash must start withspam.pow.difficulty
zeros. - If this number is greater than
spam.pow.numberOfTxPerBlock
the hash must start withspam.pow.difficulty
zeros plus one additional for eachspam.pow.numberOfTxPerBlock
sized batch of transactions beyond the limit (e.g. ifspam.pow.numberOfTxPerBlock
is 10 andspam.pow.difficulty
is 2 the 1st - 10th transactions will require 2 zeros, the 11th - 20th transactions will require 3 zeros, the 21st-30th 4 zeros, the 31st-40th 5 zeros and so on).
- If this number is less than or equal to
- The user then attaches the PoW to a transaction for a block which will be created within
spam.pow.numberOfPastBlocks
blocks of the one used for PoW hash generation, and sends it off together withx
andH(b)
.
The validators verify that:
H(b)
is the correct hash of a past block- That block is no more than
spam.pow.numberOfPastBlocks
in the past.- This check is primarily done by the leader (i.e., block creator). On agreeing on a new block, all parties check their mempool for now outdated transactions and purge them.
- The hash is computed correctly and begins with
spam.pow.difficulty
zeros, orspam.pow.difficulty
+ n zeros if the validator has seenspam.pow.numberOfTxPerBlock
* n blocks transactions from this party within the same block prior to this one. - Note that
spam.pow.numberOfTxPerBlock
is counted against the block used for PoW, not the present block. This means that a party could submit more thanspam.pow.numberOfTxPerBlock
transactions to a single created block at onlyspam.pow.difficulty
by using multiple historic blocks all withinspam.pow.numberOfPastBlocks
.
Furthermore, the validators check that:
- The same identifier has not been used for another transaction from a previously committed block. If the same identifier is used for several transactions in the same block, these transactions need to be removed during post-processing, and the initiator blocked as a spammer.
- The same block has not been used for more than
spam.pow.numberOfTxPerBlock
transactions by the same party per spam difficulty level (i.e., ifspam.pow.increaseDifficulty
is= 1
, the same block can be used for more transactions if the PoW accordingly increases in difficulty).
Notes:
- We do not require feeding the hash of the actual transaction into the hash function; this allows users to pre-compute the PoW and thus allows them to perform low latency transactions.
- As for replay protection, there is a danger that a trader communicates with a slow validator, and thus gets a wrong block number. The safest is to check validators worth > 1/3 of the weight and take the highest block hash.
Depending on how things pan out, we may have an issue with the timing; to make sure traders have sufficient time to get the block height needs us to have a large parameter of spam.pow.numberOfPastBlocks
, which may allow too many transactions. There are ways to fix this (e.g., the block height needs to end with the same bit as the validator ID), but for now we assume this doesn't cause an issue.
- The PoW is currently valid for all transactions; a consideration is to use it only for trading related transactions, so even if something goes wrong here delegators can still vote.
The initial hash-function used is SHA3. To allow for a more fine-grained control over the difficulty of the PoW (the number of zeros only allows halving/doubling), the parameter spam.pow.hashFunction
allows increasing the number of rounds of the hash function (currently 24), e.g., spam.pow.hashFunction
= sha3_36_rounds
. The parameter can in the future also be used to replace the SHA-3 through a governance vote (assuming other functions have been made available by then) should this prove necessary.
Vega nodes will periodically inspect the mempool. Any transaction sitting in the mempool with PoW tied to a historical block with number N_old
which satisfies that N_old + spam.pow.numberOfPastBlocks < N_current
will be removed from the mempool. Here N_current
is the current block the vega node is processing (has just processed).
Clients can use this simulate "time-to-live" for transactions. If the PoW is tied to a very recent block then the transaction will remain valid for (almost) the full spam.pow.numberOfPastBlocks
. If, on the other hand, the PoW is tied to very old block then the transaction will remain valid only for a few blocks; it will be included in a block soon or not at all.
All Vega clients that submitted transactions can verify that their transaction has succeeded by waiting that it's been included in a block; if they submitted a transaction with PoW tied to N_old
and N_old + spam.pow.numberOfPastBlocks < N_current
then they know the transaction was pruned and will never be included on chain.
-
A message with a missing/wrong PoW is rejected (0072-SPPW-001)
-
Reusing the same PoW for several messages is detected and the messages are rejected (0072-SPPW-002)
-
Linking too many transactions to the same block is detected and leads to rejecting the transactions that are too many (if the increasing difficulty is turned off) (0072-SPPW-003)
-
Linking too many transactions with a low difficulty level to a block is detected and leads rejecting the transactions that are too many (0072-SPPW-004)
-
Reusing a transaction identifier in a way that several transactions with the same ID end up in the same block is detected and the transactions are rejected (0072-SPPW-005)
-
PoW attached to a valid transaction will be accepted provided it's using correct chain ID and, at time of submission, the block hash is one of the last
spam.pow.numberOfPastBlocks
blocks. (0072-SPPW-007) -
For each transaction less than or equal to
spam.pow.numberOfTxPerBlock
in a blockspam.pow.difficulty
zeros are needed in the proof-of-work (0072-SPPW-008) -
For each
spam.pow.numberOfTxPerBlock
sized block of transactions greater thanspam.pow.numberOfTxPerBlock
an additional 0 is required in the proof-of-work (1 additional zero for the first batch, two additional for the second batch etc) (0072-SPPW-009) -
For a given block, a user is able to submit more than
spam.pow.numberOfTxPerBlock
transactions with onlyspam.pow.difficulty
zeros by tying them to one or more historic blocks all of which are withinspam.pow.numberOfPastBlocks
blocks (0072-SPPW-010) -
Using a block older than
spam.pow.numberOfPastBlocks
blocks prior to the current block is detected the transaction is rejected (0072-SPPW-011) -
The parameter
spam.pow.difficulty
is increased. Verify that- Transactions tied to such a block using the original difficulty are rejected with an error message.
- The effect of
pow.increaseDifficulty
is still applied, i.e., a wallet key that had the increased difficulty still has the increased difficulty w.r.t. the new baseline. (0072-SPPW-013)
-
The parameter
spam.pow.difficulty
is decreased. Verify that- Transactions tied to such a block using the new difficulty are not rejected.
- The effect of
pow.increaseDifficulty
is still applied, i.e., a wallet key that had the increased difficulty still has the increased difficulty w.r.t. the new baseline. (0072-SPPW-014)
-
The parameter
spam.pow.increaseDifficulty
is changed from 0 to 1. Verify that- Transactions tied to such a block using insufficient difficulty due to the new parameter are rejected with an error message. (0072-SPPW-015)
-
The parameter
spam.pow.increaseDifficulty
is changed from 1 to 0. Verify that- Transactions tied to such a block using the base difficulty are not rejected. (0072-SPPW-016)
-
The parameter
spam.pow.numberofTxPerBlock
is decreased. Verify that- The new parameter is used for all blocks with a block height higher than the current one, i.e., a number of transactions that was allowed before and is no longer due to the new parameter is rejected (leaving the spam difficulty level constant) (0072-SPPW-017)
-
The parameter
spam.pow.numberofTxPerBlock
is increased. Verify that- If a number of transactions is submitted with the lower lever PoW that would have exceeded the
spam.pow.numberOfTXPerBlock
before the change and not after, no transaction is rejected. (0072-SPPW-018)
- If a number of transactions is submitted with the lower lever PoW that would have exceeded the
-
Repeat tests 008-013, where in the same block,
Spam.pow.difficulty
is increased andspam.pow.increaseDifficulty
is increased (0 to 1), andspam.pow.numberOfTXPerBlock
is increased.Spam.pow.difficulty
is decreased andspam.pow.increaseDifficulty
is increased (0 to 1), andspam.pow.numberOfTXPerBlock
is increased.Spam.pow.difficulty
is increased andspam.pow.increaseDifficulty
is increased (0 to 1), andspam.pow.numberOfTXPerBlock
is decreased.Spam.pow.difficulty
is decreased andspam.pow.increaseDifficulty
is increased (0 to 1), andspam.pow.numberOfTXPerBlock
is decreased. (0072-SPPW-019)
-
Mempool pruning Cause congestion in the mempool by submitting many transactions (perhaps from several parties). Submit a transaction
T
tied to block numberN_old
. Make sure the transactions causing congestion create sufficiently largeN_current
. At some point we'll haveN_old + spam.pow.numberOfPastBlocks < N_current
and the transactionT
is removed from the mempool and never scheduled. (0072-SPPW-012)