Skip to content

Commit

Permalink
Merge pull request #800 from statechannels/ags/payment-receipt-managers
Browse files Browse the repository at this point in the history
implement Payment/Receipt managers
  • Loading branch information
andrewgordstewart authored Jul 29, 2022
2 parents eeae138 + 73deee8 commit be6722a
Show file tree
Hide file tree
Showing 5 changed files with 419 additions and 0 deletions.
62 changes: 62 additions & 0 deletions payments/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package payments

import (
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/statechannels/go-nitro/channel/state"
"github.com/statechannels/go-nitro/types"
)

type (
// A Voucher signed by Alice can be used by Bob to redeem payments in case of
// a misbehaving Alice.
//
// During normal operation, Alice & Bob would terminate the channel with an
// outcome reflecting the largest amount signed by Alice. For instance,
// - if the channel started with balances {alice: 100, bob: 0}
// - and the biggest voucher signed by alice had amount = 20
// - then Alice and Bob would cooperatively conclude the channel with outcome
// {alice: 80, bob: 20}
Voucher struct {
channelId types.Destination
amount *big.Int
signature state.Signature
}

// Balance stores the remaining and paid funds in a channel.
Balance struct {
Remaining *big.Int
Paid *big.Int
}

// PaymentManager can be used to make a payment for a given channel, issuing a new, signed voucher to be sent to the receiver
PaymentManager interface {
// Register registers a channel with a starting balance
Register(channelId types.Destination, startingBalance *big.Int) error

// Remove deletes the channel from the manager
Remove(channelId types.Destination)

// Pay will deduct amount from balance and add it to paid, returning a signed voucher for the
// total amount paid.
Pay(channelId types.Destination, amount *big.Int, pk []byte) (Voucher, error)

// Balance returns the balance of the channel
Balance(channelId types.Destination) (Balance, error)
}

ReceiptManager interface {
// Register registers a channel with a starting balance
Register(channelId types.Destination, sender common.Address, startingBalance *big.Int) error

// Remove deletes the channel from the manager
Remove(channelId types.Destination)

// Receive validates the incoming voucher, and returns the total amount received so far
Receive(voucher Voucher) (amountReceived *big.Int, err error)

// Balance returns the balance of the channel
Balance(channelId types.Destination) (Balance, error)
}
)
88 changes: 88 additions & 0 deletions payments/payment_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package payments

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/statechannels/go-nitro/types"
)

// paymentManager implements the PaymentManager interface
type paymentManager struct {
signer common.Address
channels map[types.Destination]*Balance
}

func NewPaymentManager(signer common.Address) PaymentManager {
channels := make(map[types.Destination]*Balance)
return &paymentManager{signer, channels}
}

// Register registers a channel with a starting balance
func (pm paymentManager) Register(channelId types.Destination, startingBalance *big.Int) error {
balance := &Balance{&big.Int{}, &big.Int{}}
if _, ok := pm.channels[channelId]; ok {
return fmt.Errorf("channel already registered")
}

balance.Remaining.Set(startingBalance)
pm.channels[channelId] = balance

return nil
}

// Remove deletes the channel from the manager
func (pm *paymentManager) Remove(channelId types.Destination) {
delete(pm.channels, channelId)
}

// Pay will deduct amount from balance and add it to paid, returning a signed voucher for the
// total amount paid.
func (pm *paymentManager) Pay(channelId types.Destination, amount *big.Int, pk []byte) (Voucher, error) {
balance, ok := pm.channels[channelId]
voucher := Voucher{amount: &big.Int{}}
if !ok {
return voucher, fmt.Errorf("channel not found")
}
if types.Gt(amount, balance.Remaining) {
return Voucher{}, fmt.Errorf("unable to pay amount: insufficient funds")
}

balance.Remaining.Sub(balance.Remaining, amount)
balance.Paid.Add(balance.Paid, amount)

voucher.amount.Set(balance.Paid)
voucher.channelId = channelId

if err := voucher.sign(pk); err != nil {
return Voucher{}, err
}

// question: is there a more efficient way to validate the signature against the purported signer?
// (is this validation even necessary? it's more of a failsafe than an important feature)
signer, err := voucher.recoverSigner()
if err != nil {
return Voucher{}, err
}

if signer != pm.signer {
return Voucher{}, fmt.Errorf("only signer may sign vouchers")
}

return voucher, nil
}

// Balance returns the balance of the channel
func (pm *paymentManager) Balance(channelId types.Destination) (Balance, error) {
stored, ok := pm.channels[channelId]
if !ok {
return Balance{}, fmt.Errorf("channel not found")
}

balance := Balance{&big.Int{}, &big.Int{}}
balance.Paid.Set(stored.Paid)
balance.Remaining.Set(stored.Remaining)

return balance, nil
}
130 changes: 130 additions & 0 deletions payments/payments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package payments

import (
"math/big"
"testing"

"github.com/statechannels/go-nitro/internal/testactors"
. "github.com/statechannels/go-nitro/internal/testhelpers"
"github.com/statechannels/go-nitro/types"
)

// manager lets us implement a getBalancer helper to make test assertions a little neater
type manager interface {
Balance(chanId types.Destination) (Balance, error)
}

func TestPaymentManager(t *testing.T) {
testVoucher := func(cId types.Destination, amount *big.Int, actor testactors.Actor) Voucher {
payment := &big.Int{}
payment.Set(amount)
voucher := Voucher{channelId: cId, amount: payment}
_ = voucher.sign(actor.PrivateKey)
return voucher
}

var (
channelId = types.Destination{1}
wrongChannelId = types.Destination{2}

deposit = big.NewInt(1000)
payment = big.NewInt(20)
doublePayment = big.NewInt(40)
triplePayment = big.NewInt(60)
overPayment = big.NewInt(2000)

startingBalance = Balance{big.NewInt(1000), big.NewInt(0)}
onePaymentMade = Balance{big.NewInt(980), big.NewInt(20)}
twoPaymentsMade = Balance{big.NewInt(960), big.NewInt(40)}
)

getBalance := func(m manager) Balance {
bal, _ := m.Balance(channelId)
return bal
}

// Happy path: Payment manager can register channels and make payments
paymentMgr := NewPaymentManager(testactors.Alice.Address())

_, err := paymentMgr.Pay(channelId, payment, testactors.Alice.PrivateKey)
Assert(t, err != nil, "channel must be registered to make payments")

Ok(t, paymentMgr.Register(channelId, deposit))
Equals(t, startingBalance, getBalance(paymentMgr))

firstVoucher, err := paymentMgr.Pay(channelId, payment, testactors.Alice.PrivateKey)
Ok(t, err)
Equals(t, testVoucher(channelId, payment, testactors.Alice), firstVoucher)
Equals(t, onePaymentMade, getBalance(paymentMgr))

signer, err := firstVoucher.recoverSigner()
Ok(t, err)
Equals(t, testactors.Alice.Address(), signer)

// Happy path: receipt manager can receive vouchers
receiptMgr := NewReceiptManager()

_, err = receiptMgr.Receive(firstVoucher)
Assert(t, err != nil, "channel must be registered to receive vouchers")

_ = receiptMgr.Register(channelId, testactors.Alice.Address(), deposit)
Equals(t, startingBalance, getBalance(receiptMgr))

received, err := receiptMgr.Receive(firstVoucher)
Ok(t, err)
Equals(t, received, payment)

// Receiving a voucher is idempotent
received, err = receiptMgr.Receive(firstVoucher)
Ok(t, err)
Equals(t, received, payment)
Equals(t, onePaymentMade, getBalance(receiptMgr))

// paying twice returns a larger voucher
secondVoucher, err := paymentMgr.Pay(channelId, payment, testactors.Alice.PrivateKey)
Ok(t, err)
Equals(t, testVoucher(channelId, doublePayment, testactors.Alice), secondVoucher)
Equals(t, twoPaymentsMade, getBalance(paymentMgr))

// Receiving a new voucher increases amount received
received, err = receiptMgr.Receive(secondVoucher)
Ok(t, err)
Equals(t, doublePayment, received)
Equals(t, twoPaymentsMade, getBalance(receiptMgr))

// re-registering a channel doesn't reset its balance
err = paymentMgr.Register(channelId, deposit)
Assert(t, err != nil, "expected register to fail")
Equals(t, twoPaymentsMade, getBalance(paymentMgr))

err = receiptMgr.Register(channelId, testactors.Alice.Address(), deposit)
Assert(t, err != nil, "expected register to fail")
Equals(t, twoPaymentsMade, getBalance(receiptMgr))

// Receiving old vouchers is ok
received, err = receiptMgr.Receive(firstVoucher)
Ok(t, err)
Equals(t, doublePayment, received)
Equals(t, twoPaymentsMade, getBalance(receiptMgr))

// Only the signer can sign vouchers
_, err = paymentMgr.Pay(channelId, triplePayment, testactors.Bob.PrivateKey)
Assert(t, err != nil, "only Alice can sign vouchers")

// Receiving a voucher for an unknown channel fails
_, err = receiptMgr.Receive(testVoucher(wrongChannelId, payment, testactors.Alice))
Assert(t, err != nil, "expected an error")
Equals(t, twoPaymentsMade, getBalance(receiptMgr))

// Receiving a voucher that's too large fails
_, err = receiptMgr.Receive(testVoucher(channelId, overPayment, testactors.Alice))
Assert(t, err != nil, "expected an error")
Equals(t, twoPaymentsMade, getBalance(receiptMgr))

// Receiving a voucher with the wrong signature fails
voucher := testVoucher(channelId, payment, testactors.Alice)
voucher.amount = triplePayment
_, err = receiptMgr.Receive(voucher)
Assert(t, err != nil, "expected an error")
Equals(t, twoPaymentsMade, getBalance(receiptMgr))
}
91 changes: 91 additions & 0 deletions payments/receipt_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package payments

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/statechannels/go-nitro/types"
)

type (
// paymentStatus stores the status of payments for a given payment channel.
paymentStatus struct {
channelSender common.Address
startingBalance *big.Int
largestVoucher Voucher
}

// receiptManager receives vouchers, validates them, and stores the most valuable voucher
receiptManager struct {
channels map[types.Destination]*paymentStatus
}
)

func NewReceiptManager() ReceiptManager {
channels := make(map[types.Destination]*paymentStatus)
return &receiptManager{channels}
}

// Register registers a channel for use, given the sender and starting balance of the channel
func (pm receiptManager) Register(channelId types.Destination, sender common.Address, startingBalance *big.Int) error {
balance := &big.Int{}
balance.Set(startingBalance)
voucher := Voucher{channelId: channelId, amount: big.NewInt(0)}
data := &paymentStatus{sender, balance, voucher}
if _, ok := pm.channels[channelId]; ok {
return fmt.Errorf("channel already registered")
}

pm.channels[channelId] = data

return nil
}

// Remove deletes the channel's status
func (pm *receiptManager) Remove(channelId types.Destination) {
delete(pm.channels, channelId)
}

// Receive validates the incoming voucher, and returns the total amount received so far
func (rm *receiptManager) Receive(voucher Voucher) (*big.Int, error) {
status, ok := rm.channels[voucher.channelId]
if !ok {
return &big.Int{}, fmt.Errorf("channel not registered")
}

received := &big.Int{}
received.Set(voucher.amount)
if types.Gt(received, status.startingBalance) {
return &big.Int{}, fmt.Errorf("channel has insufficient funds")
}

receivedSoFar := status.largestVoucher.amount
if !types.Gt(received, receivedSoFar) {
return receivedSoFar, nil
}

signer, err := voucher.recoverSigner()
if err != nil {
return &big.Int{}, err
}
if signer != status.channelSender {
return &big.Int{}, fmt.Errorf("wrong signer: %+v, %+v", signer, status.channelSender)
}

status.largestVoucher = voucher
return received, nil
}

// Balance returns the balance of the channel
func (rm *receiptManager) Balance(channelId types.Destination) (Balance, error) {
data, ok := rm.channels[channelId]
if !ok {
return Balance{}, fmt.Errorf("channel not found")
}

balance := Balance{&big.Int{}, &big.Int{}}
balance.Paid.Set(data.largestVoucher.amount)
balance.Remaining.Sub(data.startingBalance, data.largestVoucher.amount)
return balance, nil
}
Loading

0 comments on commit be6722a

Please sign in to comment.