diff --git a/payments/api.go b/payments/api.go new file mode 100644 index 000000000..660c3423e --- /dev/null +++ b/payments/api.go @@ -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) + } +) diff --git a/payments/payment_manager.go b/payments/payment_manager.go new file mode 100644 index 000000000..b487c5222 --- /dev/null +++ b/payments/payment_manager.go @@ -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 +} diff --git a/payments/payments_test.go b/payments/payments_test.go new file mode 100644 index 000000000..ca596b19c --- /dev/null +++ b/payments/payments_test.go @@ -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)) +} diff --git a/payments/receipt_manager.go b/payments/receipt_manager.go new file mode 100644 index 000000000..d37cd587f --- /dev/null +++ b/payments/receipt_manager.go @@ -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 +} diff --git a/payments/vouchers.go b/payments/vouchers.go new file mode 100644 index 000000000..23519bcbd --- /dev/null +++ b/payments/vouchers.go @@ -0,0 +1,48 @@ +package payments + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/crypto" + nitroAbi "github.com/statechannels/go-nitro/abi" + nitroCrypto "github.com/statechannels/go-nitro/crypto" + "github.com/statechannels/go-nitro/types" +) + +func (v Voucher) hash() (types.Bytes32, error) { + encoded, err := abi.Arguments{ + {Type: nitroAbi.Destination}, + {Type: nitroAbi.Uint256}, + }.Pack(v.channelId, v.amount) + + if err != nil { + return types.Bytes32{}, fmt.Errorf("failed to encode voucher: %w", err) + } + return crypto.Keccak256Hash(encoded), nil +} + +func (v *Voucher) sign(pk []byte) error { + hash, err := v.hash() + if err != nil { + return err + } + + sig, err := nitroCrypto.SignEthereumMessage(hash.Bytes(), pk) + + if err != nil { + return err + } + + v.signature = sig + + return nil +} + +func (v Voucher) recoverSigner() (types.Address, error) { + h, error := v.hash() + if error != nil { + return types.Address{}, error + } + return nitroCrypto.RecoverEthereumMessageSigner(h[:], v.signature) +}