Payments

This document describes a probabilistic micropayment mechanism used facilitate payments between relay nodes, producers and consumers.

Constants

Name Type Value

| BLOCKS_IN_EPOCH | uint | TBD | | UNLOCK_TIMEOUT | uint | TBD |

Storage

Expected Payment

struct expectedPayment {
    uint count
    uint lastPayment
}

Data Structures

Expected Payments

mapping(bytes32 => expectedPayment) expectedPayments

Operation

Relayer gets the chunk from the parent, verifies the witness and checks if the message attestation is correct. Chunk is dropped if the witness is invalid and if attestation is incorrect and witness is valid, then it is reported to the spam prevention contract. The chunk is propogated if both attestation and witness are valid and match the chunk data.

Setup

Relayers and Consumers needs to setup an escrow and fund the escrow in order to pay the relayers who propogate the data to the node.

Payment Escrow

Data Structures

struct UnlockRequest {
    address sender
    uint timestamp
    uint amount
}

Storage:

mapping(address => uint) lockedBalances
mapping(id => UnlockRequest) unlockRequests
mapping(address => uint) unlockedBalances

uint UNLOCK_TIMEOUT
address tokenContract
string tokenSymbol
ERC20 erc20
contract PaymentEscrow {
    // We support only ERC20 as of now
    function addEscrow(address tokenContract, uint amount) {
        erc20::transferFrom(msg.sender, address(this), amount)
        lockedBalances[msg.sender] += amount
        emit BalanceChanged(msg.sender)
    }

    funtion unlock(uint amount) {
        unlockId = keccak256(msg.sender, now, amount)
        unlockRequests[unlockId] = UnlockRequest(msg.sender, now, amount)
        return unlockId
    }

    function sealUnlockRequest(bytes32 unlockId) {
        require(unlockRequests[unlockId].timestamp + UNLOCK_TIMEOUT > now)
        lockedBalances[sender] -= unlockRequests[id].amount
        unlockedBalances[sender] += unlockRequests[id].amount
    }

    function withdraw(uint amount) {
        require(amount <= unlockedBalances[msg.sender])
        unlockedBalances[msg.sender] -= amount
        erc20.transfer(amount)
    }

    function pay(address from, address to, address value) {
        // check if the calling contract is authorized to pay
        require(value <= unlockedBalances[from])
        lockedBalances[from] -= value
        unlockedBalances[to] += value
    }

    function getAvailableBalance(address user) {
        return lockedBalances[user]
    }
}

Probablistic payments

After verifying that the message is valid and to be broadcasted, the relayer creates the witness. The message along with the witness are relayed to the identified child connections. The child node should send a receipt in response for the data relayed to the child. The format of the receipt is as below.

Receipt Format

| Relayer Address | Relay Fee | Timestamp | Stake Size | Stake Offset | Receiver Signature |

<--- 20 Bytes --> <- 12 Bytes -> <- 4 Bytes -> <-- 4 Byte --> <-- 4 Bytes -> <---- 65 Bytes ---->

 func onMessage(Message msg):
    witness = msg.witness
    fee, paymentMode = getFee(witness.relayer, msg.size)
    receipt = createReceipt(witness, fee)
    peer(witness.relayer).send('receipt', receipt)
func createReceipt(Witness witness, uint fee, uint paymentMode):
    receiptPayload = witness.relayer + fee + Date() + paymentMode
    signature = ECDSA.sign(receiptPayload, self.privKey)
    receipt = receiptPayload + signature
    return receipt
func getFee(bytes32 relayer, uint messageSize):
    // Fee is adjusted based on Multi arm bandit solution approach to minimize the total fee while maintaining same level of performance
    // A custom logic can be implemented for selecting the payment method
    // TODO: Provide a default logic for selecting paymentMethod

Winning Range

Winning range is the range of hashes between which any ticket will be able to redeem transaction fee. The transaction fee redeemed by the relayer is (Base reward factor) * (Relay Fee). Base reward factor is calculated based on the width of the winning range for the timestamp of the ticket.

Base Reward Factor = 2^256/(Winning range)

// TODO: Dynamic adjustment of Base reward factor has to be revisited because a malicious actor keeps giving tickets to himself and submits them to blockchain increasing the redemption rate artificially which decreases the rewards for everyone. So someone can fakely decrease the amount of payments happenning in the network. So dynamic adjustment must be well thought of.

Base Reward Factor is adjusted after every BLOCKS_IN_EPOCH blocks based on number of redemptions of transaction fee in the epoch. If the number of redemptions per epoch are less than the TARGET_REDEMPTIONS then the winning range is widened and if the number of redemptions is less than the TARGET_REDEMPTIONS then the winning range is shortened. The winning range is widened and shortened in constant steps to ensure that the expected rewards are not drastically affected. Based on adjusted winning range, base reward factor is recalculated.

redemptionDiff = TARGET_REDEMPTIONS - numberOfRedemptions

winningRangeChangeStep = (Winning range)*WINNING_RANGE_STEP_FACTOR/2^256

Winning range starts from 0 and the range varies as per the number of redemptions.

Payment Contract

contract Payment {
    function redeem(Receipt signedReceipt) {
        // check if the timestamp matches the epoch
        require(signedReceipt.timestamp > LAST_EPOCH_END_TIME && signedReceipt.timestamp < LAST_EPOCH_END_TIME + BLOCKS_IN_EPOCH)
        // check if the receipt is valid and the sender has enough money to pay
        receiptPayload = signedReceipt.relayer + signedReceipt.fee + signedReceipt.timestamp + signedReceipt.paymentMode
        sender = ECDSA.verify(receiptPayload, signedReceipt.signature)
        escrowAddress = getPaymentEscrowAddress(signedReceipt.paymentMode)
        require(isValidPayee(sender, signedReceipt.fee, escrowAddress))
        // check if the relayer address matched the address that signed the receipt
        receiptPayload += receipt.signature
        signedRelayer = ECDSA.verify(receiptPayload, signedReceipt.relayerSignature)
        require(signedRelayer == signedReceipt.relayer)
        // check if the ticket matches the winning range
        ticket = keccak256(signedReceipt)
        require(hasTicketWon(ticket))
        // reward the relayer from the payer fee escrow
        Pay(signedRelayer, sender, signedReceipt.fee*BaseRewardFactor, escrowAddress)
    }

    function isValidPayee(address payee, uint fee, bytes32 escrowAddress) {
        availableBalance = PaymentEscrow(escrowAddress)::getAvailableBalance(payee)
        return require(availableBalance >= fee*BaseRewardFactor)
    }

    function hasTicketWon(bytes32 ticket) {
        // TODO: Winning Range management(including BaseRewardFactor) can come from another contract
        require(ticket < WinningRange)
    }

    function getPaymentEscrowAddress(uint paymentMode) {

    }

    function Pay(address from, address to, uint value, bytes32 escrowAddress) internal {
        require(PaymentEscrow(escrowAddress)::pay(from, to, value))
    }
}

Redeem the ticket

Receipt once received will be signed by the relayer and the hash of the signed receipt is the ticket that has to be compared to the winning range for the ticket. If the ticket wins then a transaction is sent to the payments contract to redeem the transaction relay fee before the end of next epoch.

func onReceipt(Receipt receipt):
    currentTimestamp = Date()
    if receipt.relayer != self.pubKey || (receipt.timestamp < currentTimestamp && receipt.timestamp - currentTimestamp > BLOCKS_IN_EPOCH ):
        return
    sender = verifyReceipt(receipt)
    receipt.sender = sender
    signedReceipt, ticket = calculateTicket(receipt)
    if hasTicketWon(ticket):
        redeemTicket(signedReceipt)
func calculateTicket(Receipt receipt):
    receiptPayload = receipt.relayer + receipt.fee + receipt.timestamp + receipt.paymentMode + receipt.signature
    receipt.relayerSignature = ECDSA.sign(receiptPayload, self.privKey)
    ticket = keccak256(receipt)
    return receipt, ticket
func hasTicketWon(bytes32 ticket):
    if ticket < WINNING_RANGE :
        return true
    return false
func redeemTicket(Receipt signedReceipt):
    Payment::redeem(signedReceipt)

Payments

Payments are made by creating a micropayment channels between producers and cluster admins. These admins are also connected to each other to ensure that producer need not have a micropayment channel to all the cluster admins.