This repository has been archived by the owner on Feb 24, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🔀 Merge branch '250-elaborate-funder-tests' into 'dev'
[eth/channel] More elaborate Funder tests #250 Closes #250 See merge request perun/go-perun!184
- Loading branch information
Showing
19 changed files
with
1,921 additions
and
189 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
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,112 @@ | ||
// Copyright (c) 2020 Chair of Applied Cryptography, Technische Universität | ||
// Darmstadt, Germany. All rights reserved. This file is part of go-perun. Use | ||
// of this source code is governed by a MIT-style license that can be found in | ||
// the LICENSE file. | ||
|
||
package channel // import "perun.network/go-perun/backend/ethereum/channel" | ||
|
||
import ( | ||
"context" | ||
"math/big" | ||
|
||
"github.com/ethereum/go-ethereum/accounts/abi/bind" | ||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/ethereum/go-ethereum/core/types" | ||
"github.com/pkg/errors" | ||
|
||
"perun.network/go-perun/backend/ethereum/bindings/adjudicator" | ||
"perun.network/go-perun/channel" | ||
"perun.network/go-perun/log" | ||
psync "perun.network/go-perun/pkg/sync" | ||
) | ||
|
||
// compile time check that we implement the perun adjudicator interface | ||
var _ channel.Adjudicator = (*Adjudicator)(nil) | ||
|
||
// The Adjudicator struct implements the channel.Adjudicator interface | ||
// It provides all functionality to close a channel. | ||
type Adjudicator struct { | ||
ContractBackend | ||
contract *adjudicator.Adjudicator | ||
// The address to which we send all funds. | ||
Receiver common.Address | ||
// Structured logger | ||
log log.Logger | ||
// Transaction mutex | ||
mu psync.Mutex | ||
} | ||
|
||
// NewAdjudicator creates a new ethereum adjudicator. | ||
func NewAdjudicator(backend ContractBackend, contract common.Address, onchainAddress common.Address) *Adjudicator { | ||
contr, err := adjudicator.NewAdjudicator(contract, backend) | ||
if err != nil { | ||
panic("Could not create a new instance of adjudicator") | ||
} | ||
return &Adjudicator{ | ||
ContractBackend: backend, | ||
contract: contr, | ||
Receiver: onchainAddress, | ||
log: log.WithField("account", backend.account.Address), | ||
} | ||
} | ||
|
||
func (a *Adjudicator) callRegister(ctx context.Context, req channel.AdjudicatorReq) error { | ||
return a.call(ctx, req, a.contract.Register) | ||
} | ||
|
||
func (a *Adjudicator) callRefute(ctx context.Context, req channel.AdjudicatorReq) error { | ||
return a.call(ctx, req, a.contract.Refute) | ||
} | ||
|
||
func (a *Adjudicator) callConclude(ctx context.Context, req channel.AdjudicatorReq) error { | ||
// Wrapped call to Conclude, ignoring sig | ||
conclude := func( | ||
opts *bind.TransactOpts, | ||
params adjudicator.ChannelParams, | ||
state adjudicator.ChannelState, | ||
_ [][]byte, | ||
) (*types.Transaction, error) { | ||
return a.contract.Conclude(opts, params, state) | ||
} | ||
return a.call(ctx, req, conclude) | ||
} | ||
|
||
func (a *Adjudicator) callConcludeFinal(ctx context.Context, req channel.AdjudicatorReq) error { | ||
return a.call(ctx, req, a.contract.ConcludeFinal) | ||
} | ||
|
||
type adjFunc = func( | ||
opts *bind.TransactOpts, | ||
params adjudicator.ChannelParams, | ||
state adjudicator.ChannelState, | ||
sigs [][]byte, | ||
) (*types.Transaction, error) | ||
|
||
// call calls the given contract function `fn` with the data from `req`. | ||
// `fn` should be a method of `a.contract`, like `a.contract.Register`. | ||
func (a *Adjudicator) call(ctx context.Context, req channel.AdjudicatorReq, fn adjFunc) error { | ||
ethParams := channelParamsToEthParams(req.Params) | ||
ethState := channelStateToEthState(req.Tx.State) | ||
tx, err := func() (*types.Transaction, error) { | ||
if !a.mu.TryLockCtx(ctx) { | ||
return nil, errors.Wrap(ctx.Err(), "context canceled while acquiring tx lock") | ||
} | ||
defer a.mu.Unlock() | ||
|
||
trans, err := a.newTransactor(ctx, big.NewInt(0), GasLimit) | ||
if err != nil { | ||
return nil, errors.WithMessage(err, "creating transactor") | ||
} | ||
tx, err := fn(trans, ethParams, ethState, req.Tx.Sigs) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "calling adjudicator function") | ||
} | ||
log.Debugf("Sent transaction %v", tx.Hash().Hex()) | ||
return tx, nil | ||
}() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return errors.WithMessage(confirmTransaction(ctx, a.ContractBackend, tx), "mining transaction") | ||
} |
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,149 @@ | ||
// Copyright (c) 2020 Chair of Applied Cryptography, Technische Universität | ||
// Darmstadt, Germany. All rights reserved. This file is part of go-perun. Use | ||
// of this source code is governed by a MIT-style license that can be found in | ||
// the LICENSE file. | ||
|
||
package channel | ||
|
||
import ( | ||
"context" | ||
"math/big" | ||
"math/rand" | ||
"testing" | ||
"time" | ||
|
||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
"perun.network/go-perun/backend/ethereum/channel/test" | ||
"perun.network/go-perun/backend/ethereum/wallet" | ||
ethwallettest "perun.network/go-perun/backend/ethereum/wallet/test" | ||
"perun.network/go-perun/channel" | ||
channeltest "perun.network/go-perun/channel/test" | ||
perunwallet "perun.network/go-perun/wallet" | ||
wallettest "perun.network/go-perun/wallet/test" | ||
) | ||
|
||
func newAdjudicator(t *testing.T, rng *rand.Rand, n int) ([]perunwallet.Account, []perunwallet.Address, []*Funder, []*Adjudicator, common.Address) { | ||
ctx, cancel := context.WithTimeout(context.Background(), timeout) | ||
defer cancel() | ||
simBackend := test.NewSimulatedBackend() | ||
ks := ethwallettest.GetKeystore() | ||
deployAccount := wallettest.NewRandomAccount(rng).(*wallet.Account).Account | ||
simBackend.FundAddress(ctx, deployAccount.Address) | ||
contractBackend := NewContractBackend(simBackend, ks, deployAccount) | ||
// Deploy Adjudicator | ||
adjudicator, err := DeployAdjudicator(context.Background(), contractBackend) | ||
require.NoError(t, err, "Deploying the adjudicator should not error") | ||
// Deploy Assetholder | ||
assetETH, err := DeployETHAssetholder(ctx, contractBackend, adjudicator) | ||
require.NoError(t, err, "Deploying asset holder failed") | ||
t.Logf("asset holder address is %v", assetETH) | ||
t.Logf("adjudicator address is %v", adjudicator) | ||
accs := make([]perunwallet.Account, n) | ||
parts := make([]perunwallet.Address, n) | ||
funders := make([]*Funder, n) | ||
adjudicators := make([]*Adjudicator, n) | ||
for i := 0; i < n; i++ { | ||
acc := wallettest.NewRandomAccount(rng).(*wallet.Account) | ||
simBackend.FundAddress(ctx, acc.Account.Address) | ||
accs[i] = acc | ||
parts[i] = acc.Address() | ||
cb := NewContractBackend(simBackend, ks, acc.Account) | ||
funders[i] = NewETHFunder(cb, assetETH) | ||
adjudicators[i] = NewAdjudicator(cb, adjudicator, deployAccount.Address) | ||
} | ||
return accs, parts, funders, adjudicators, assetETH | ||
} | ||
|
||
func signState(t *testing.T, accounts []perunwallet.Account, params *channel.Params, state *channel.State) channel.Transaction { | ||
// Sign valid state. | ||
sigs := make([][]byte, len(accounts)) | ||
for i := 0; i < len(accounts); i++ { | ||
sig, err := Sign(accounts[i], params, state) | ||
assert.NoError(t, err, "Sign should not return error") | ||
sigs[i] = sig | ||
} | ||
return channel.Transaction{ | ||
State: state, | ||
Sigs: sigs, | ||
} | ||
} | ||
|
||
func TestSubscribeRegistered(t *testing.T) { | ||
seed := time.Now().UnixNano() | ||
t.Logf("seed is %v", seed) | ||
rng := rand.New(rand.NewSource(int64(seed))) | ||
// create new Adjudicator | ||
accs, parts, funders, adjs, asset := newAdjudicator(t, rng, 1) | ||
// create valid state and params | ||
app := channeltest.NewRandomApp(rng) | ||
params := channel.NewParamsUnsafe(uint64(100*time.Second), parts, app.Def(), big.NewInt(rng.Int63())) | ||
state := newValidState(rng, params, asset) | ||
// Set up subscription | ||
ctx, cancel := context.WithTimeout(context.Background(), timeout) | ||
defer cancel() | ||
registered, err := adjs[0].SubscribeRegistered(ctx, params) | ||
assert.NoError(t, err, "Subscribing to valid params should not error") | ||
// we need to properly fund the channel | ||
fundingCtx, funCancel := context.WithTimeout(context.Background(), timeout) | ||
defer funCancel() | ||
// fund the contract | ||
reqFund := channel.FundingReq{ | ||
Params: params, | ||
Allocation: &state.Allocation, | ||
Idx: channel.Index(0), | ||
} | ||
require.NoError(t, funders[0].Fund(fundingCtx, reqFund), "funding should succeed") | ||
// Now test the register function | ||
ctxReg, regCancel := context.WithTimeout(context.Background(), timeout) | ||
defer regCancel() | ||
tx := signState(t, accs, params, state) | ||
req := channel.AdjudicatorReq{ | ||
Params: params, | ||
Acc: accs[0], | ||
Idx: channel.Index(0), | ||
Tx: tx, | ||
} | ||
event, err := adjs[0].Register(ctxReg, req) | ||
assert.NoError(t, err, "Registering state should succeed") | ||
assert.Equal(t, event, registered.Next(), "Events should be equal") | ||
assert.NoError(t, registered.Close(), "Closing event channel should not error") | ||
assert.Nil(t, registered.Next(), "Next on closed channel should produce nil") | ||
assert.NoError(t, registered.Err(), "Closing should produce no error") | ||
// Setup a new subscription | ||
registered2, err := adjs[0].SubscribeRegistered(ctx, params) | ||
assert.NoError(t, err, "registering two subscriptions should not fail") | ||
assert.Equal(t, event, registered2.Next(), "Events should be equal") | ||
assert.NoError(t, registered2.Close(), "Closing event channel should not error") | ||
assert.Nil(t, registered2.Next(), "Next on closed channel should produce nil") | ||
assert.NoError(t, registered2.Err(), "Closing should produce no error") | ||
} | ||
|
||
func newValidState(rng *rand.Rand, params *channel.Params, assetholder common.Address) *channel.State { | ||
// Create valid state. | ||
assets := []channel.Asset{ | ||
&Asset{Address: assetholder}, | ||
} | ||
ofparts := make([][]channel.Bal, len(params.Parts)) | ||
for i := 0; i < len(ofparts); i++ { | ||
ofparts[i] = make([]channel.Bal, len(assets)) | ||
for k := 0; k < len(assets); k++ { | ||
ofparts[i][k] = big.NewInt(rng.Int63n(999) + 1) | ||
} | ||
} | ||
allocation := channel.Allocation{ | ||
Assets: assets, | ||
OfParts: ofparts, | ||
Locked: []channel.SubAlloc{}, | ||
} | ||
|
||
return &channel.State{ | ||
ID: params.ID(), | ||
Version: 4, | ||
App: params.App, | ||
Allocation: allocation, | ||
Data: channeltest.NewRandomData(rng), | ||
IsFinal: false, | ||
} | ||
} |
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
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,81 @@ | ||
// Copyright (c) 2020 Chair of Applied Cryptography, Technische Universität | ||
// Darmstadt, Germany. All rights reserved. This file is part of go-perun. Use | ||
// of this source code is governed by a MIT-style license that can be found in | ||
// the LICENSE file. | ||
|
||
package channel // import "perun.network/go-perun/backend/ethereum/channel" | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/pkg/errors" | ||
"perun.network/go-perun/backend/ethereum/bindings/adjudicator" | ||
|
||
"perun.network/go-perun/channel" | ||
) | ||
|
||
// ensureConcluded ensures that conclude or concludeFinal (for non-final and | ||
// final states, resp.) is called on the adjudicator. | ||
// - a subscription on Concluded events is established | ||
// - it searches for a past concluded event | ||
// - if found, channel is already concluded and success is returned | ||
// - if none found, conclude/concludeFinal is called on the adjudicator | ||
// - it waits for a Concluded event from the blockchain | ||
func (a *Adjudicator) ensureConcluded(ctx context.Context, req channel.AdjudicatorReq) error { | ||
// Listen for Concluded event. | ||
watchOpts, err := a.newWatchOpts(ctx) | ||
if err != nil { | ||
return errors.WithMessage(err, "creating watchOpts") | ||
} | ||
concluded := make(chan *adjudicator.AdjudicatorConcluded) | ||
sub, err := a.contract.WatchConcluded(watchOpts, concluded, [][32]byte{req.Params.ID()}) | ||
if err != nil { | ||
return errors.Wrap(err, "WatchConcluded failed") | ||
} | ||
defer sub.Unsubscribe() | ||
|
||
if found, err := a.filterConcluded(ctx, req.Params.ID()); err != nil { | ||
return errors.WithMessage(err, "filtering old Concluded events") | ||
} else if found { | ||
return nil | ||
} | ||
|
||
// No conclude event found in the past, send transaction. | ||
if req.Tx.IsFinal { | ||
err = errors.WithMessage(a.callConcludeFinal(ctx, req), "calling concludeFinal") | ||
} else { | ||
err = errors.WithMessage(a.callConclude(ctx, req), "calling conclude") | ||
} | ||
if IsTxFailedError(err) { | ||
a.log.Warn("Calling conclude(Final) failed, waiting for event anyways...") | ||
} else if err != nil { | ||
return err | ||
} | ||
|
||
select { | ||
case <-concluded: | ||
return nil | ||
case <-ctx.Done(): | ||
return errors.Wrap(ctx.Err(), "context cancelled") | ||
case err = <-sub.Err(): | ||
return errors.Wrap(err, "subscription error") | ||
} | ||
} | ||
|
||
// filterConcluded returns whether there has been a Concluded event in the past. | ||
func (a *Adjudicator) filterConcluded(ctx context.Context, channelID channel.ID) (bool, error) { | ||
filterOpts, err := a.newFilterOpts(ctx) | ||
if err != nil { | ||
return false, err | ||
} | ||
iter, err := a.contract.FilterConcluded(filterOpts, [][32]byte{channelID}) | ||
if err != nil { | ||
return false, errors.Wrap(err, "creating iterator") | ||
} | ||
|
||
if !iter.Next() { | ||
return false, errors.Wrap(iter.Error(), "iterating") | ||
} | ||
// Event found | ||
return true, nil | ||
} |
Oops, something went wrong.