-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #800 from statechannels/ags/payment-receipt-managers
implement Payment/Receipt managers
- Loading branch information
Showing
5 changed files
with
419 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.