2020-11-13 06:02:17 +00:00
## SWAP implements Accounting for Waku. See
## https://github.com/vacp2p/specs/issues/24 for more.
##
## This is based on the SWAP based approach researched by the Swarm team, and
## can be thought of as an economic extension to Bittorrent's tit-for-tat
## economics.
##
## It is quite suitable for accounting for imbalances between peers, and
## specifically for something like the Store protocol.
##
## It is structured as follows:
##
## 1) First a handshake is made, where terms are agreed upon
##
## 2) Then operation occurs as normal with HistoryRequest, HistoryResponse etc
## through store protocol (or otherwise)
##
## 3) When payment threshhold is met, a cheque is sent. This acts as promise to
## pay. Right now it is best thought of as karma points.
##
## Things like settlement is for future work.
##
2021-07-19 08:49:27 +00:00
{. push raises : [ Defect ] . }
# TODO Generally clean up errors here, a lot of Exceptions, Defects and KeyErros
#
# On KeyEror specifically:
2021-07-15 18:25:52 +00:00
# Accessing Table's items is prone to KeyError exception when the key does not belong to the table
2021-07-19 08:49:27 +00:00
# such exception can be avoided by calling hasKey() before accessing the key (which is the case in this module)
2021-07-15 18:25:52 +00:00
# but from the compiler point of view, the use of hasKey() does not make any difference in the potential exceptions
2021-07-19 08:49:27 +00:00
# - thus any key access should be wrapped inside try-except
# - or otherwise the exception should be thrown by the proc and handled by the higher level calls
2021-07-15 18:25:52 +00:00
2020-11-13 06:02:17 +00:00
import
2021-02-25 02:47:48 +00:00
std / [ tables , options , json ] ,
2020-11-13 06:02:17 +00:00
bearssl ,
2020-11-20 13:59:29 +00:00
chronos , chronicles , metrics , stew / results ,
2020-11-13 06:02:17 +00:00
libp2p / crypto / crypto ,
libp2p / protocols / protocol ,
libp2p / protobuf / minprotobuf ,
libp2p / stream / connection ,
2021-03-26 08:49:51 +00:00
.. / .. / node / peer_manager / peer_manager ,
2021-02-23 05:15:06 +00:00
. / waku_swap_types ,
.. / .. / waku / v2 / protocol / waku_swap / waku_swap_contracts
2020-11-13 06:02:17 +00:00
2020-11-23 02:27:45 +00:00
export waku_swap_types
2020-11-13 06:02:17 +00:00
2021-06-17 15:36:44 +00:00
const swapAccountBalanceBuckets = [ - Inf , - 200 .0 , - 150 .0 , - 100 .0 , - 50 .0 , 0 .0 , 50 .0 , 100 .0 , 150 .0 , 200 .0 , Inf ]
declarePublicGauge waku_swap_peers_count , " number of swap peers "
2021-01-29 08:42:41 +00:00
declarePublicGauge waku_swap_errors , " number of swap protocol errors " , [ " type " ]
2021-06-17 15:36:44 +00:00
declarePublicHistogram waku_peer_swap_account_balance , " Swap Account Balance for waku peers, aggregated into buckets based on threshold limits " , buckets = swapAccountBalanceBuckets
2021-01-29 08:42:41 +00:00
2020-11-13 06:02:17 +00:00
logScope :
topics = " wakuswap "
2021-05-28 16:13:17 +00:00
const WakuSwapCodec * = " /vac/waku/swap/2.0.0-beta1 "
2020-11-13 06:02:17 +00:00
2021-02-09 08:31:38 +00:00
# Error types (metric label values)
const
dialFailure = " dial_failure "
decodeRpcFailure = " decode_rpc_failure "
2020-11-24 04:53:42 +00:00
# Serialization
# -------------------------------------------------------------------------------
2020-11-13 06:02:17 +00:00
proc encode * ( handshake : Handshake ) : ProtoBuffer =
2021-07-15 18:25:52 +00:00
var output = initProtoBuffer ( )
output . write ( 1 , handshake . beneficiary )
return output
2020-11-13 06:02:17 +00:00
proc encode * ( cheque : Cheque ) : ProtoBuffer =
2021-07-15 18:25:52 +00:00
var output = initProtoBuffer ( )
output . write ( 1 , cheque . beneficiary )
output . write ( 2 , cheque . date )
output . write ( 3 , cheque . amount )
output . write ( 4 , cheque . signature )
return output
2020-11-13 06:02:17 +00:00
proc init * ( T : type Handshake , buffer : seq [ byte ] ) : ProtoResult [ T ] =
var beneficiary : seq [ byte ]
var handshake = Handshake ( )
let pb = initProtoBuffer ( buffer )
discard ? pb . getField ( 1 , handshake . beneficiary )
2021-07-15 18:25:52 +00:00
return ok ( handshake )
2020-11-13 06:02:17 +00:00
proc init * ( T : type Cheque , buffer : seq [ byte ] ) : ProtoResult [ T ] =
var beneficiary : seq [ byte ]
var date : uint32
var amount : uint32
2021-02-23 05:15:06 +00:00
var signature : seq [ byte ]
2020-11-13 06:02:17 +00:00
var cheque = Cheque ( )
let pb = initProtoBuffer ( buffer )
discard ? pb . getField ( 1 , cheque . beneficiary )
discard ? pb . getField ( 2 , cheque . date )
discard ? pb . getField ( 3 , cheque . amount )
2021-02-23 05:15:06 +00:00
discard ? pb . getField ( 4 , cheque . signature )
2020-11-13 06:02:17 +00:00
2021-07-15 18:25:52 +00:00
return ok ( cheque )
2020-11-16 09:55:49 +00:00
# Accounting
2020-11-24 04:53:42 +00:00
# -------------------------------------------------------------------------------
2020-11-16 09:55:49 +00:00
#
2020-11-24 04:53:42 +00:00
# We credit and debits peers based on what for now is a form of Karma asset.
# TODO Test for credit/debit operations in succession
2020-11-16 09:55:49 +00:00
2021-02-23 05:15:06 +00:00
# TODO Assume we calculated cheque
2021-06-22 20:55:01 +00:00
proc sendCheque * ( ws : WakuSwap , peerInfo : PeerInfo ) {. async . } =
let connOpt = await ws . peerManager . dialPeer ( peerInfo , WakuSwapCodec )
2021-02-09 08:31:38 +00:00
if connOpt . isNone ( ) :
# @TODO more sophisticated error handling here
error " failed to connect to remote peer "
waku_swap_errors . inc ( labelValues = [ dialFailure ] )
return
2020-11-26 10:02:10 +00:00
info " sendCheque "
2021-02-23 05:15:06 +00:00
# TODO We get this from the setup of swap setup, dynamic, should be part of setup
2020-11-26 10:02:10 +00:00
# TODO Add beneficiary, etc
2021-02-23 05:15:06 +00:00
var aliceSwapAddress = " 0x6C3d502f1a97d4470b881015b83D9Dd1062172e1 "
2021-06-18 15:53:51 +00:00
var aliceWalletAddress = " 0x6C3d502f1a97d4470b881015b83D9Dd1062172e1 "
2021-02-25 02:47:48 +00:00
var signature : string
var res = waku_swap_contracts . signCheque ( aliceSwapAddress )
if res . isOk ( ) :
2021-03-01 05:55:20 +00:00
info " signCheque " , res = res [ ]
2021-02-25 02:47:48 +00:00
let json = res [ ]
signature = json [ " signature " ] . getStr ( )
else :
# To test code paths, this should look different in a production setting
warn " Something went wrong when signing cheque, sending anyway "
2021-02-23 05:15:06 +00:00
2021-06-18 15:53:51 +00:00
info " Signed Cheque " , swapAddress = aliceSwapAddress , signature = signature , issuerAddress = aliceWalletAddress
2021-02-23 05:15:06 +00:00
let sigBytes = cast [ seq [ byte ] ] ( signature )
2021-06-18 15:53:51 +00:00
await connOpt . get ( ) . writeLP ( Cheque ( amount : 1 , signature : sigBytes , issuerAddress : aliceWalletAddress ) . encode ( ) . buffer )
2020-11-26 10:02:10 +00:00
# Set new balance
2021-06-22 20:55:01 +00:00
let peerId = peerInfo . peerId
2020-11-26 10:02:10 +00:00
ws . accounting [ peerId ] - = 1
info " New accounting state " , accounting = ws . accounting [ peerId ]
# TODO Authenticate cheque, check beneficiary etc
2021-07-19 08:49:27 +00:00
proc handleCheque * ( ws : WakuSwap , cheque : Cheque , peerInfo : PeerInfo ) {. raises : [ Defect , KeyError ] . } =
2020-11-26 10:02:10 +00:00
info " handle incoming cheque "
2021-06-22 20:55:01 +00:00
let peerId = peerInfo . peerId
2021-03-01 05:55:20 +00:00
2021-06-18 15:53:51 +00:00
# Get the original signer using web3. For now, a static value (0x6C3d502f1a97d4470b881015b83D9Dd1062172e1) will be used.
# Check if web3.eth.personal.ecRecover(messageHash, signature); or an equivalent function has been implemented in nim-web3
let signer = " 0x6C3d502f1a97d4470b881015b83D9Dd1062172e1 "
# Verify that the Issuer was the signer of the signature
if signer ! = cheque . issuerAddress :
warn " Invalid cheque: The address of the issuer is different from the signer. "
2021-03-01 05:55:20 +00:00
# TODO Redeem cheque here
var signature = cast [ string ] ( cheque . signature )
# TODO Where should Alice Swap Address come from? Handshake probably?
# Hacky for now
var aliceSwapAddress = " 0x6C3d502f1a97d4470b881015b83D9Dd1062172e1 "
info " Redeeming cheque with " , swapAddress = aliceSwapAddress , signature = signature
var res = waku_swap_contracts . redeemCheque ( aliceSwapAddress , signature )
if res . isOk ( ) :
info " redeemCheque ok " , redeem = res [ ]
else :
info " Unable to redeem cheque "
# Check balance here
# TODO How do we get ERC20 address here?
# XXX This one is wrong
# Normally this would be part of initial setup, otherwise we need some temp persistence here
# Possibly as part of handshake?
var erc20address = " 0x6C3d502f1a97d4470b881015b83D9Dd1062172e1 "
let balRes = waku_swap_contracts . getERC20Balances ( erc20address )
if balRes . isOk ( ) :
# XXX: Assumes Alice and Bob here...
var bobBalance = balRes [ ] [ " bobBalance " ] . getInt ( )
info " New balance is " , balance = bobBalance
else :
info " Problem getting Bob balance "
# TODO Could imagine scenario where you don't cash cheque but leave it as credit
# In that case, we would probably update accounting state, but keep track of cheques
# When this is true we update accounting state anyway when node is offline,
# makes waku_swap test pass for now
# Consider desired logic here
var stateUpdateOverRide = true
if res . isOk ( ) :
info " Updating accounting state with redeemed cheque "
ws . accounting [ peerId ] + = int ( cheque . amount )
else :
if stateUpdateOverRide :
info " Updating accounting state with even if cheque failed "
ws . accounting [ peerId ] + = int ( cheque . amount )
else :
info " Not updating accounting state with due to bad cheque "
2020-11-26 10:02:10 +00:00
info " New accounting state " , accounting = ws . accounting [ peerId ]
2021-06-06 15:15:18 +00:00
# Log Account Metrics
proc logAccountMetrics * ( ws : Wakuswap , peer : PeerId ) {. async . } =
2021-06-17 15:36:44 +00:00
waku_peer_swap_account_balance . observe ( ws . accounting [ peer ] . int64 )
2021-06-06 15:15:18 +00:00
2020-11-18 12:45:51 +00:00
proc init * ( wakuSwap : WakuSwap ) =
info " wakuSwap init 1 "
proc handle ( conn : Connection , proto : string ) {. async , gcsafe , closure . } =
2020-11-26 10:02:10 +00:00
info " swap handle incoming connection "
var message = await conn . readLp ( 64 * 1024 )
# XXX This can be handshake, etc
var res = Cheque . init ( message )
if res . isErr :
error " failed to decode rpc "
2021-02-09 08:31:38 +00:00
waku_swap_errors . inc ( labelValues = [ decodeRpcFailure ] )
2020-11-26 10:02:10 +00:00
return
info " received cheque " , value = res . value
2021-06-22 20:55:01 +00:00
wakuSwap . handleCheque ( res . value , conn . peerInfo )
2020-11-18 12:45:51 +00:00
2021-07-19 08:49:27 +00:00
proc credit ( peerInfo : PeerInfo , n : int )
{. gcsafe , closure , raises : [ Defect , KeyError , Exception ] . } =
2021-06-22 20:55:01 +00:00
let peerId = peerInfo . peerId
2021-05-26 10:05:56 +00:00
info " Crediting peer: " , peer = peerId , amount = n
2020-11-24 04:53:42 +00:00
if wakuSwap . accounting . hasKey ( peerId ) :
wakuSwap . accounting [ peerId ] - = n
else :
wakuSwap . accounting [ peerId ] = - n
info " Accounting state " , accounting = wakuSwap . accounting [ peerId ]
2021-06-22 20:55:01 +00:00
wakuSwap . applyPolicy ( peerInfo )
2020-11-26 10:02:10 +00:00
2020-11-24 04:53:42 +00:00
# TODO Debit and credit here for Karma asset
2021-07-19 08:49:27 +00:00
proc debit ( peerInfo : PeerInfo , n : int )
{. gcsafe , closure , raises : [ Defect , KeyError , Exception ] . } =
2021-06-22 20:55:01 +00:00
let peerId = peerInfo . peerId
2021-05-26 10:05:56 +00:00
info " Debiting peer: " , peer = peerId , amount = n
2020-11-18 12:45:51 +00:00
if wakuSwap . accounting . hasKey ( peerId ) :
wakuSwap . accounting [ peerId ] + = n
else :
wakuSwap . accounting [ peerId ] = n
info " Accounting state " , accounting = wakuSwap . accounting [ peerId ]
2021-06-22 20:55:01 +00:00
wakuSwap . applyPolicy ( peerInfo )
2021-05-26 10:05:56 +00:00
2021-07-19 08:49:27 +00:00
proc applyPolicy ( peerInfo : PeerInfo )
{. gcsafe , closure , raises : [ Defect , KeyError , Exception ] . } =
2021-06-22 20:55:01 +00:00
let peerId = peerInfo . peerId
2021-05-26 10:05:56 +00:00
# TODO Separate out depending on if policy is soft (accounting only) mock (send cheque but don't cash/verify) hard (actually send funds over testnet)
#Check if the Disconnect Threshold has been hit. Account Balance nears the disconnectThreshold after a Credit has been done
2021-06-15 02:06:36 +00:00
if wakuSwap . accounting [ peerId ] < = wakuSwap . config . disconnectThreshold :
warn " Disconnect threshhold has been reached: " , threshold = wakuSwap . config . disconnectThreshold , balance = wakuSwap . accounting [ peerId ]
2021-05-26 10:05:56 +00:00
else :
info " Disconnect threshhold not hit "
2020-11-18 12:45:51 +00:00
2021-05-26 10:05:56 +00:00
#Check if the Payment threshold has been hit. Account Balance nears the paymentThreshold after a Debit has been done
2021-06-15 02:06:36 +00:00
if wakuSwap . accounting [ peerId ] > = wakuSwap . config . paymentThreshold :
warn " Payment threshhold has been reached: " , threshold = wakuSwap . config . paymentThreshold , balance = wakuSwap . accounting [ peerId ]
2021-05-26 10:05:56 +00:00
#In soft phase we don't send cheques yet
2021-06-15 02:06:36 +00:00
if wakuSwap . config . mode = = Mock :
2021-06-22 20:55:01 +00:00
discard wakuSwap . sendCheque ( peerInfo )
2020-11-26 10:02:10 +00:00
else :
info " Payment threshhold not hit "
2021-06-06 15:15:18 +00:00
waitFor wakuSwap . logAccountMetrics ( peerId )
2020-11-18 12:45:51 +00:00
wakuSwap . handler = handle
wakuSwap . codec = WakuSwapCodec
2020-11-24 04:53:42 +00:00
wakuSwap . credit = credit
wakuSwap . debit = debit
2021-05-26 10:05:56 +00:00
wakuswap . applyPolicy = applyPolicy
2020-11-18 12:45:51 +00:00
2020-11-24 04:53:42 +00:00
# TODO Expression return?
2021-06-15 02:06:36 +00:00
proc init * ( T : type WakuSwap , peerManager : PeerManager , rng : ref BrHmacDrbgContext , swapConfig : SwapConfig ) : T =
2020-11-18 12:45:51 +00:00
info " wakuSwap init 2 "
2021-07-15 18:25:52 +00:00
let
accounting = initTable [ PeerId , int ] ( )
text = " test "
var ws = WakuSwap ( rng : rng ,
peerManager : peerManager ,
accounting : accounting ,
text : text ,
config : swapConfig )
ws . init ( )
return ws
2020-11-16 09:55:49 +00:00
2020-11-26 10:02:10 +00:00
proc setPeer * ( ws : WakuSwap , peer : PeerInfo ) =
2021-02-11 08:58:25 +00:00
ws . peerManager . addPeer ( peer , WakuSwapCodec )
2021-06-17 15:36:44 +00:00
waku_swap_peers_count . inc ( )
2020-11-26 10:02:10 +00:00
2020-11-16 09:55:49 +00:00
# TODO End to end communication