Spam Prevention

Data is created by producers and transmitted across the network by clusters any of whom can be malicious including producers. This document defines the mechanism to ensure that malicious relayers or clusters or producers or consumers cannot attack the network and degrade network performance by sending spam or invalid messages.

Concepts

Attestation

Attestation is a signature by the sender of the message to make sure that the message is not a spam and slash stake of the sender in case it is.

Constants

Name Type Value
REPORT_INVALID_WITNESS_REWARD uint TBD
MAX_CHUNK_SIZE uint 2**32 = 4 GB
PRODUCER_MESSAGE_STAKE_LOCKTIME uint TBD
PRODUCER_STAKE_PER_BYTE uint TBD
RELAYER_MESSAGE_STAKE_LOCKTIME uint TBD
RELAYER_STAKE_PER_BYTE uint TBD

Data Structures

Type Aliases

Name Type Description
MessageId bytes32 Message Id

Attestation

struct Attestation {
    bytes message
    bytes2 channelId
    bytes messageId
    bytes2 chunkNumber
    bytes4 timestamp
    bytes32 nextHopAddress
    bytes32 sender
    bytes signature
}

Message

struct Message {
    MessageId messageId
    bool isReported
    Attestation attestation
    bytes witness
}

Storage

Message Cache

mapping(MessageId => Message) messageCache

Report Cache

mapping(MessageId => Message) reportCache

Salt Store

mapping(MessageId => Salt) saltStore

Operations

There are multiple components where spam is possible, below are the list of possible spam

  • Message Integrity
    • Witness integrity
    • Attestation integrity
  • Message Replay
  • Producer Spam Burst
  • Relayer Stake Capped Payments
  • Invalid chunk propogation
  • Changing attestation source
  • Infinite Valid Chunks
  • Block Integrity
    • Low Difficulty Spam
    • MessageId
    • Signature
    • Header Structure
    • Difficulty and nonce
    • Transactions body structure
    • Merkle root of transactions
    • Tx verification

Message Integrity

Chunks are propogated from source to various destinations through clusters among which some might be malicious. This section specifies the protections in place to prevent malicious clusters from tampering with the intergrity of the message. Thus avoid spam of message which might not be correct.

There are three major components to the data that is propogated, which are chunk, attestation and witness. There might exist malicious clusters who propogate messages with wrong chunk/attestation/witness in the network resulting in spam of the network by invalid messages. Hence the following procedures are defined to penalise based on the validity of message relayed.

The following procedure happens at receiver

func verifyMessage(Message msg):
    witness = msg.witness
    attestation = msg.attestation
    attestationMessage = msg.data
    messageHash = keccak256(attestationMessage)
    if messageHash != witness.messageHash
        return false
    chunkHash == keccak256(msg.chunk)
    if chunkHash != attestation.chunkHash
        return false
    isWitnessValid = verifyWitness(witness)
    if !isWitnessValid
        return false
    attestor = verifyAttestation(attestation)
    if !attestor && checkSufficientStake(witness.attestor)
        SpamPrevention::reportInvalidAttestationRelay(attestation, wintess)
        return false

Attestation

Each chunk that is broadcasted to the network needs to be attested by an attester which ensures that the chunk is not invalid or spam. The attestation is verified by each receiver, messages with no/invalid attestation and no/invalid witness as well are dropped as they might be sent by a malicious actor who doesn't have any stake in the network. Any cluster who has relayed an invalid attestation will be slashed.

Attestation reporting contract

Attestation reporting contract verifies if a invalid attestation was relayed with a valid witness and slashes the relayer who signed the witness.

func reportInvalidAttestationRelay(Attestation attestation, Witness witness):
    if signatureVerify(attestation.messageId + attestation.channelId + attestation.chunkId + attestation.timestamp + attestation.chunkLength + attestation.stakeOffset + attestation.chunkHash, attestation.signature) == true:
        return
    if signatureVerify(witness.stakeOffset + witness.messageSize + attestation.chunkHash, witness.signature) == true:
        slash(witness.relayer)

Witness Integrity

Witness is verified for each of the message received to ensure the integrity of the message being relayed. If an invalid witness is provided the message is dropped. As it is not possible to attribute responsibility verifiably if an invalid witness is provided, the sender of the invalid witness cannot be slashed but the node can make a decision on the behaviour of the parent/cluster based on the knowledge available locally even if it cannot be proved verifiably.

func verifyWitness(Witness witness):
    witnessPayload = witness.stakeOffset + witness.messageSize + witness.timestamp + attestation.chunkHash
    if witness.relayer == ECDSA.verify(witnessPayload, witness.signature)
        return true
    return false

Message Replay

A malicious actor can resend a valid message which was previously sent in the network. Messages are cached for MESSAGE_CACHE_TIMEOUT and then deleted. If the message is already present in the cache then it is dropped. If the message is not in the cache and the timestamp of the message is older than MESSAGE_CACHE_TIMEOUT then the message is discarded. This mechanism ensures that both recent and older messages cannot be spammed in the network.

func verifyReplayMessage(Message message):
    id = msg.attestation.messageId + msg.attestation.channelId + msg.attestation.chunkId
    if messageCache[id]:
        return false
    if now - message.timestamp > MESSAGE_CACHE_TIMEOUT
        return false
    return true

Producer Spam Burst

A malicious producer who is ready to incur stake slashing can send huge burst of messages which incur more loss to the network than the slashed stake. The receivers might be overwhelmed with chunks such that it is hard for receivers to come up with a set of chunks to reconstruct the message resulting in DoS for the receivers. PRODUCER_STAKE_PER_BYTE is the amount of LIN to be staked to send one byte of data in the network. Non fungible tokens of value PRODUCER_STAKE_PER_BYTE are created for the LIN staked by producer. When a producer sends messages in the network the portion of LIN that are staked for the message should be mentioned. If two messages have overlapping portions of LIN staked, then the newer message is dropped and both the messages are reported to the contract, so that corresponding stake is slashed. The portion of stake mentioned in the message can be reused after a cooldown period of PRODUCER_MESSAGE_STAKE_LOCKTIME.

func onMessage(Message msg):
    stakeOffset = msg.attestation.stakeOffset
    stakeEndOffset = msg.attestation.stakeOffset + msg.attestation.chunkLength * PRODUCER_STAKE_PER_BYTE
    overlappingMessage = findRecentStakeOverlappingMessage(stakeOffset, stakeEndOffset)
    if !overlappingMessage :
        return
    SpamPrevention::reportOverlappingMessages(msg.attestation, overlappingMessage.attestation)

Reporting messages with overlapping Stake

contract SpamPrevention {
    function reportOverlappingProducerStakes(Attestation attestation1, Attestation attestation2):
        if attestation1.timestamp > attestation2.timestamp :
            producerTimedifference = attestation1.timestamp - attestation2.timestamp
        else
            producerTimedifference = attestation2.timestamp - attestation1.timestamp
        require(producerTimedifference < PRODUCER_MESSAGE_STAKE_LOCKTIME)
        attestor1 = ECDSA.verify(attestation1.messageId + attestation1.channelId + attestation1.chunkId + attestation1.timestamp + attestation1.chunkLength + attestation1.stakeOffset + attestation1.chunkHash, attestation1.signature)
        attestor2 = ECDSA.verify(attestation2.messageId + attestation2.channelId + attestation2.chunkId + attestation2.timestamp + attestation2.chunkLength + attestation2.stakeOffset + attestation2.chunkHash, attestation2.signature)
        require(attestor1 == attestor2)
        attestation1StartStake = attestation1.stakeOffset
        attestation1EndStake = attestation1.chunkLength * PRODUCER_STAKE_PER_BYTE
        attestation2StartStake = attestation2.stakeOffset
        attestation2EndStake = attestation2.chunkLength * PRODUCER_STAKE_PER_BYTE
        require(max(attestation1StartStake, attestation2StartStake) <= min(attestation1EndStake, attestation2EndStake))
        slash(attestor1)
}

Effect of slashing on stake portions

In case of slashing all the stake attached to messages with overlapping stake portions are slashed. The stake portion that is slashed cannot be used again as an event will be emitted when the stake portion is slashed and all nodes need to listen and keep track of such events. Any future message with a slashed stake portion will be dropped and the relayer can be reported for transmitting a message with a slashed stake portion. Once the producer exits by withdrawing the stake all the relevant messages about stake portions are removed.

Effect of clock sync on slashing

A message is transmitted in the network only if the timestamp of the attestation is lower than the cluster node local timestamp. If the attestation timestamp differs from the cluster entry node timestamp by more than a threshold time higher than the local timestmap, the message is not accepted by the node. This is to ensure that the cluster doesn't end up violating the SLA because of the wrong timestamp. If the timestamp is lower than the timestamp of the node, then the producer is giving more time for the receiver to send the message. Hence it is not benificial for the producer to send the messages.

Invalid Chunk

A malicious producer can send invalid chunks which cannot be used to reconstruct the original message. In this case, anyone who received MIN_CHUNKS[message_type] should be able to reconstuct the message and if they are unable to, then one of the chunks must be invalid. Hence MIN_CHUNKS[message_type] with which the reconstructed message didn't match the structure of the block for the message_type can be submitted to the contract and the source of the message will be slashed.

contract InvalidChunks {
    function report(Chunk[] chunk) public {
        // TODO: erasure coding reconstruction
    }

    function isValidChunk(Chunk chunk) internal {
        // TODO: check chunk validity
    }
}

Chunk Withholding

A malicious producer can withhold chunks from being propagated in the network by not publishing them to the network and probably paying less fee as well. But this assumes that most of the clusters are honest because of which even if the producer doesn't propagate all chunks, the message will still reach the receivers. This can be done as part of the protocol as well by reducing the redundency ratio if the producer feels that honesty assumptions can be higher. If the blocks aren't reconstructable then the receiver raised a dispute and the producer needs to submit enough acks to ensure that the data is not withheld.

Infinite Valid Chunks

A malicious producer can send infinite valid chunks to spam the network with too many chunks. In this case, if anyone submits more than NUM_CHUNKS[message_type] chunks headers for the same messageId and producer, then the producer of the chunks is slashed.

contract InvalidChunks {
    function report(Chunk[] chunks, bytes32 messageId, bytes2 channelId) public {
        require(chunks.length > NUM_CHUNKS[channelId])
        for(uint256 i = 0; i < chunks.length; i++) {
            require(chunks[i].messageId == messageId)
            require(validChunk(chunks[i]))
        }
    }
}

Block Integrity

Spam check for block integrity can be done by any relayer or consumer of the message. Spam check is dependent on the Blockchain Protocol whose block/transaction is being sent through the Network. If a spam of a detected block/transaction either by the node or informed by the spam reporting contract, it is added to reportCache. If the same spam is detected by the node, it is voted.

Spam reporting

Spam check for block integrity can be done by any relayer or consumer of the message. Spam check is dependent on the Blockchain Protocol whose block/transaction is being sent through the Network. If a spam of a detected block/transaction either by the node or informed by the spam reporting contract, it is added to reportCache. If the same spam is detected by the node, it is voted.

Spam reporting

Node listens to the Spam reporting contract and adds a new spam report to reportCache. If a new spam is detected, it is checked against reportCache, if already exists then spam is voted for and opens new report if doesn't already exist.

func onSpam(MessageId messageId, Message message, Attestation attestation):
        reportSpam(messageId)
func reportSpam(MessageId messageId, Address producerAddress):
    stakeLIN(SPAM_REPORT_STAKE, address(SpamReportingContract))
    SpamReportingContract::reportSpam(this.reportStaker.pubKey, messageId, producerAddress).send(this.privKey)
func stakeLIN(uint amount, Address allocatedTo):
    LINContract::approve(this.reportStaker.pubKey, allocatedTo, amount).send(this.reportStaker.privKey)

Spam reporting contract

Once the prediction market comes to a decision, a transcation is sent to get the result from the prediction market. This can be invoked by anyone.

func onPredictionMarketCompletion(MessageId messageId):
    SpamReportingContract::getReportResult(messageId).send(this.privKey)

Spam reporting contract helps network manage spam detection, voting and slashing the source of spam. As the result of the computation is known offchain to everyone with the data to reconstruct the block, the spam can be reported to the contract. A prediction market is run using a platform like Augur and using open reporting by providing a significant reward which might be lower than the reward given to the reporter. The data can be made available by the reporter to everyone as part of the prediction market. Then it is simpler for the users participating in the prediction market to decide on the outcome. The result of the outcome from the prediction market will be used to decide on the spam.

The following is the message used for operations

"If we use rabin's IDA reconstruction algorithm on the following inputs, will it result in a spam block for the $BLOCKCHAIN blockchain. Inputs are all signed by the same person $PRODUCER_ADDRESS and message id $MESSAGE_ID". Link to inputs are given .

Spam reporting contract helps network manage spam detection, voting and slashing the source of spam. As the result of the computation is known offchain to everyone with the data to reconstruct the block, the spam can be reported to the contract. A prediction market is run using a platform like Augur and using open reporting by providing a significant reward which might be lower than the reward given to the reporter. The data can be made available by the reporter to everyone as part of the prediction market. Then it is simpler for the users participating in the prediction market to decide on the outcome. The result of the outcome from the prediction market will be used to decide on the spam.

The following is the message used for operations

"If we use rabin's IDA reconstruction algorithm on the following inputs, will it result in a spam block for the $BLOCKCHAIN blockchain. Inputs are all signed by the same person $PRODUCER_ADDRESS and message id $MESSAGE_ID". Link to inputs are given .

Storage
map((bytes32 messageId, address sender) -> Report) reports
map(bytes32 messageId -> uint) revealedVotes
Operations
func reportSpam(reportStaker, messageId, producerAddress):
    require(LINContract.transfer(reportStaker, address(this), SPAM_REPORT_STAKE))
    marketId = PredictionMarket.create(question)
    reports[messageId][msg.sender] = Report(now, messageId, reportStaker, producerAddress, marketId)
func getReportResult(messageId):
    if(PredictionMarket.getResult(marketId) == true)
        Report report = reports[messageId]
        slash(report.producer)
        reward(messageId, reportStaker)
        delete reports[messageId]
    selfdestruct()

<!-- ### Transaction spam prevention

Spam prevention in case of transactions can be limited based on the stake that the producer has put. As stake is limit, number of transactions that can be broadcasted is also limited. But transaction propogation might not be a very good idea because the economics are not in favour of propogation.

If a transaction has to be propogated the only people who will be interested in buying the transaction is miners and the only way the transaction will be propogated is if there are users who are waiting to pay and by propogating it wide, that fee can be claimed. But in this case it is opposite, and hence there is no incentive for a intermediate who is not a miner to propogate tx as the reward is not sufficient as the number of miners are small the potential of this intermediate reaching a miner is low, hence the reward he might get is low. So the only way to incentivize transaction propogation is by paying high fee for the transactions and hence providing enough incentive for a intermediate to propogate. This defeats the purpose as you will probably end up paying high tx fee compared to right now, as you might have to single handedly incentivize all the intermediates to propogate. As it should make sense for the miner to pay for the tx.

func getReportResult(messageId):
    if(PredictionMarket.getResult(marketId) == true)
        Report report = reports[messageId]
        slash(report.producer)
        reward(messageId, reportStaker)
        delete reports[messageId]
    selfdestruct()