Spam Prevention

This document defines the mechanism in Marlin protocol to avoid spam by either Marlin Relayers or Producers/consumers who use the protocol.

Data is created by producers and transmitted across the network by Marlin relayers.

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.

Data Structures

Type Aliases

Name Type Description
MessageId bytes32 Message Id
Message bytes Message sent through Marlin network

Attestation

struct Attestation {
    bytes32 chunkId
    bytes32 signature
}

Storage

Report Cache

mapping(MessageId => Message) reportCache

Salt Store

mapping(MessageId => Salt) saltStore

Operations

Message attestation

Each message needs to be attested as not spam by an attester. The attestation is verified by each node, messages with bad/no attestations are reported and entire witness path penalised.

Bad attestation reporting

Any relayer or consumer of the chunk will verify the received chunk for attestation. If the attestation isn't present or doesn't match the chunk, then it is reported to smart contract.

func onNewChunk(Attestation attestation, bytes[] witness):
    if signatureVerify(attestation.chunkId, attestation.signature) == false:
        reportAttestation(attestation, witness)
func reportAttestation(attestation, witness):
    SpamPrevention::reportAttestation(attestation, witness)

Attestation reporting contract

Attestation reporting contract verifies if the attesting is present and valid. If not slashes every relayer in the witness.

func reportAttestation(Attestation attestation, bytes[] witness):
    if signatureVerify(attestation.chunkId, attestation.signature) == true:
        return
    for node in parseNodesFromWitness(witness):
        slashStake(node)

Spam check

Spam check 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. (TODO: Confirm conditions for reporting and vote)

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 onNewReport(MessageId messageId, Message message):  // On contract event
    reportCache[messageId] = message
func onSpam(MessageId messageId, Message message, Attestation attestation):
    salt = Math.rand()
    hashedVote = keccak256(message, salt)
    if reportCache.contains(messageId):
        voteSpam(messageId, hashedVote)
        saltStore[messageId] = salt
        startRevealVoteTimer(messageId)
    else:
        reportSpam(messageId, message, attestation, hashedVote)
func voteSpam(MessageId messageId, bytes32 hashedVote):
    SpamReportingContract::voteSpam(messageId, hashedVote)
func reportSpam(MessageId messageId, bytes[] message, Attestation attestation, bytes32 hashedVote):
    SpamReportingContract::reportSpam(messageId, message, attestation, hashedVote)

Once the voting phase is completed, vote is revealed by sending the actual vote that matches the commitment.

func onRevealVoteTimer(messageId):
    revealVote(messageId)
func revealVote(messageId):
    salt = saltStore[messageId]
    SpamReportingContract::revealVote(messageId, salt)

Spam reporting contract

Spam reporting contract helps network manage spam detection, voting and the slashing conditions. Spam can only be reported by relayers. A sealed vote is published to the contract and are revealed once the voting stage is completed. Once the reveal stage is completed, the number of votes revealed helps slash the sender of spam message.

Storage
map(bytes32 messageId -> (uint startTime, bytes32 attestation, bytes[] message)) reports
map((bytes32 messageId, address sender) -> bytes32 hashedVote) committedVotes
map(bytes32 messageId -> uint) revealedVotes
Operations
func voteSpam(messageId, hashedVote):
    committedVotes[(messageId, sender)] = hashedVote
func reportSpam(messageId, message, attestation, hashedVote):
    require(NetworkContract.isRelayerInNetwork(msg.sender))
    reports[messageId] = (now, attestation, message)
    committedVotes[(messageId, sender)] = hashedVote
    emit NewReport(messageId, message)
func revealVote(messageId, salt):
    if hash(yes, salt) == committedVotes[(messageId, sender)]:
        revealedVotes[messageId]++
        else if hash(no, salt) == committedVotes[(messageId, sender)]:
        revealedVotes[messageId]--
func sealVote(messageId):
    if currentTime > reports[messageId].startTime + 600 and revealedVotes[messageId] > 0:
        slashStake(reports[messageId].attestation.attester)
        reports.remove(messageId)