Skip to content

Commit 12c78d3

Browse files
committed
🔀 Merge branch '435-simple-wallet' into 'dev'
[eth/wallet/ecdsa] ecdsa secret key wallet implementation Closes hyperledger-labs#435 See merge request perun/go-perun!396
2 parents cf9279c + ddd2a46 commit 12c78d3

File tree

8 files changed

+440
-2
lines changed

8 files changed

+440
-2
lines changed

NOTICE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Chair of Applied Cryptography, Technische Universität Darmstadt, Germany
2727
Norbert Dzikowski <norbert@perun.network>
2828
Matthias Geihs <matthias@perun.network>
2929
Luigi Iandolo <luigi@perun.network>
30+
Philipp-Florens Lehwalder <philipp@perun.network>
3031
Steffen Rattay <steffen@perun.network>
3132
Sebastian Stammler <seb@perun.network>
3233
Oliver Tale-Yazdi <oliver@perun.network>
@@ -35,6 +36,7 @@ Chair of Applied Cryptography, Technische Universität Darmstadt, Germany
3536
PolyCrypt GmbH
3637
Norbert Dzikowski <norbert@perun.network>
3738
Matthias Geihs <matthias@perun.network>
39+
Philipp-Florens Lehwalder <philipp@perun.network>
3840
Steffen Rattay <steffen@perun.network>
3941
Sebastian Stammler <seb@perun.network>
4042
Oliver Tale-Yazdi <oliver@perun.network>

backend/ethereum/wallet/hd/wallet_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@ func TestUnlock(t *testing.T) {
8585

8686
missingAddr := common.BytesToAddress(setup.AddressBytes)
8787
_, err := hdWallet.Unlock(ethwallet.AsWalletAddr(missingAddr))
88-
assert.Error(t, err, "should not error on unlocking missing address")
88+
assert.Error(t, err, "should error on unlocking missing address")
8989

9090
validAcc, _ := setup.UnlockedAccount()
9191
acc, err := hdWallet.Unlock(validAcc.Address())
92-
assert.NoError(t, err, "should not error on unlocking missing address")
92+
assert.NoError(t, err, "should not error on unlocking valid address")
9393
assert.NotNil(t, acc, "account should be non nil when error is nil")
9494
}
9595

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2021 - See NOTICE file for copyright holders.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package simple
16+
17+
import (
18+
"crypto/ecdsa"
19+
20+
"github.com/ethereum/go-ethereum/accounts"
21+
"github.com/ethereum/go-ethereum/crypto"
22+
"github.com/pkg/errors"
23+
24+
ethwallet "perun.network/go-perun/backend/ethereum/wallet"
25+
"perun.network/go-perun/wallet"
26+
)
27+
28+
// Account represents an account held in the simple wallet.
29+
type Account struct {
30+
accounts.Account
31+
key *ecdsa.PrivateKey
32+
}
33+
34+
// Address returns the Ethereum address of this account.
35+
func (a *Account) Address() wallet.Address {
36+
return ethwallet.AsWalletAddr(a.Account.Address)
37+
}
38+
39+
// SignData is used to sign data with this account.
40+
func (a *Account) SignData(data []byte) ([]byte, error) {
41+
hash := ethwallet.PrefixedHash(data)
42+
sig, err := a.SignHash(hash)
43+
if err != nil {
44+
return nil, errors.Wrap(err, "SignHash")
45+
}
46+
sig[64] += 27
47+
return sig, nil
48+
}
49+
50+
// SignHash is used to sign an already prefixed hash with this account.
51+
func (a *Account) SignHash(hash []byte) ([]byte, error) {
52+
return crypto.Sign(hash, a.key)
53+
}
54+
55+
// createAccount creates an account using the given private key.
56+
func createAccount(k *ecdsa.PrivateKey) *Account {
57+
return &Account{
58+
Account: accounts.Account{Address: crypto.PubkeyToAddress(k.PublicKey)},
59+
key: k,
60+
}
61+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2021 - See NOTICE file for copyright holders.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package simple contains a simplistic implementation of the perun wallet,
16+
// account, and transactor interfaces.
17+
// A simple wallet can be instantiated directly from a list of secret keys. It
18+
// is meant to be used for testing and demonstration purposes only. It should
19+
// not be used in production.
20+
package simple // import "perun.network/go-perun/backend/ethereum/wallet/simple"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2021 - See NOTICE file for copyright holders.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package simple
16+
17+
import (
18+
"github.com/ethereum/go-ethereum/accounts"
19+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
20+
"github.com/ethereum/go-ethereum/common"
21+
"github.com/ethereum/go-ethereum/core/types"
22+
"github.com/pkg/errors"
23+
)
24+
25+
// TransactorFactory can be used to make TransactOpts for Accounts stored in a wallet.
26+
type TransactorFactory struct {
27+
*Wallet
28+
types.Signer
29+
}
30+
31+
// NewTransactor returns a TransactOpts for the given account. It errors if the
32+
// account is not contained in the wallet of the transactor factory.
33+
func (t *TransactorFactory) NewTransactor(account accounts.Account) (*bind.TransactOpts, error) {
34+
acc, ok := t.Wallet.Accounts[account.Address]
35+
if !ok {
36+
return nil, errors.New("account not found in wallet")
37+
}
38+
return &bind.TransactOpts{
39+
From: account.Address,
40+
Signer: func(address common.Address, tx *types.Transaction) (*types.Transaction, error) {
41+
if address != account.Address {
42+
return nil, errors.New("not authorized to sign this account")
43+
}
44+
45+
signature, err := acc.SignHash(t.Signer.Hash(tx).Bytes())
46+
if err != nil {
47+
return nil, err
48+
}
49+
return tx.WithSignature(t.Signer, signature)
50+
},
51+
}, nil
52+
}
53+
54+
// NewTransactor returns a TransactorFactory that can make TransactOpts for
55+
// accounts contained in the given simple wallet.
56+
func NewTransactor(w *Wallet, signer types.Signer) *TransactorFactory {
57+
return &TransactorFactory{Wallet: w, Signer: signer}
58+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2021 - See NOTICE file for copyright holders.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package simple_test
16+
17+
import (
18+
"math/big"
19+
"math/rand"
20+
"testing"
21+
22+
"github.com/ethereum/go-ethereum/accounts"
23+
"github.com/ethereum/go-ethereum/common"
24+
"github.com/ethereum/go-ethereum/core/types"
25+
"github.com/stretchr/testify/require"
26+
27+
"perun.network/go-perun/backend/ethereum/channel/test"
28+
"perun.network/go-perun/backend/ethereum/wallet"
29+
"perun.network/go-perun/backend/ethereum/wallet/simple"
30+
pkgtest "perun.network/go-perun/pkg/test"
31+
)
32+
33+
// Missing address for which key will not be contained in the wallet.
34+
const missingAddr = "0x1"
35+
36+
func TestTxOptsBackend(t *testing.T) {
37+
rng := pkgtest.Prng(t)
38+
chainID := rng.Int63()
39+
40+
tests := []struct {
41+
title string
42+
signer types.Signer
43+
chainID int64
44+
}{
45+
{
46+
title: "FrontierSigner",
47+
signer: &types.FrontierSigner{},
48+
chainID: 0,
49+
},
50+
{
51+
title: "HomesteadSigner",
52+
signer: &types.HomesteadSigner{},
53+
chainID: 0,
54+
},
55+
{
56+
title: "EIP155Signer",
57+
signer: types.NewEIP155Signer(big.NewInt(chainID)),
58+
chainID: chainID,
59+
},
60+
}
61+
62+
for _, _t := range tests {
63+
_t := _t
64+
t.Run(_t.title, func(t *testing.T) {
65+
s := newTransactorSetup(t, rng, _t.signer, _t.chainID)
66+
test.GenericSignerTest(t, rng, s)
67+
})
68+
}
69+
}
70+
71+
func newTransactorSetup(t require.TestingT, prng *rand.Rand, signer types.Signer, chainID int64) test.TransactorSetup {
72+
simpleWallet := simple.NewWallet()
73+
require.NotNil(t, simpleWallet)
74+
validAcc := simpleWallet.NewRandomAccount(prng)
75+
require.NotNil(t, validAcc)
76+
77+
return test.TransactorSetup{
78+
Signer: signer,
79+
ChainID: chainID,
80+
Tr: simple.NewTransactor(simpleWallet, signer),
81+
ValidAcc: accounts.Account{Address: wallet.AsEthAddr(validAcc.Address())},
82+
MissingAcc: accounts.Account{Address: common.HexToAddress(missingAddr)},
83+
}
84+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2021 - See NOTICE file for copyright holders.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package simple
16+
17+
import (
18+
"crypto/ecdsa"
19+
"math/rand"
20+
21+
"github.com/ethereum/go-ethereum/accounts"
22+
"github.com/ethereum/go-ethereum/common"
23+
"github.com/ethereum/go-ethereum/crypto"
24+
"github.com/ethereum/go-ethereum/crypto/secp256k1"
25+
"github.com/pkg/errors"
26+
27+
ethwallet "perun.network/go-perun/backend/ethereum/wallet"
28+
"perun.network/go-perun/log"
29+
"perun.network/go-perun/wallet"
30+
)
31+
32+
var _ wallet.Wallet = (*Wallet)(nil)
33+
34+
// Wallet is a simple wallet.Wallet implementation holding a map of all included Accounts.
35+
type Wallet struct {
36+
Accounts map[common.Address]*Account
37+
}
38+
39+
// NewWallet creates a new Wallet with Accounts corresponding to the privateKeys.
40+
func NewWallet(privateKeys ...*ecdsa.PrivateKey) *Wallet {
41+
accs := make(map[common.Address]*Account)
42+
for _, key := range privateKeys {
43+
addr := crypto.PubkeyToAddress(key.PublicKey)
44+
accs[addr] = createAccount(key)
45+
}
46+
return &Wallet{Accounts: accs}
47+
}
48+
49+
// Contains checks whether this wallet contains the account corresponding to the given address.
50+
func (w *Wallet) Contains(addr common.Address) bool {
51+
_, ok := w.Accounts[addr]
52+
return ok
53+
}
54+
55+
// NewRandomAccount creates a new pseudorandom account using the provided
56+
// randomness. The returned account is already unlocked.
57+
func (w *Wallet) NewRandomAccount(prng *rand.Rand) wallet.Account {
58+
privateKey, err := ecdsa.GenerateKey(secp256k1.S256(), prng)
59+
if err != nil {
60+
log.Panicf("Creating account: %v", err)
61+
}
62+
63+
addr := crypto.PubkeyToAddress(privateKey.PublicKey)
64+
if !w.Contains(addr) {
65+
acc := Account{
66+
Account: accounts.Account{Address: addr},
67+
key: privateKey,
68+
}
69+
w.Accounts[addr] = &acc
70+
return &acc
71+
}
72+
return w.Accounts[addr]
73+
}
74+
75+
// Unlock returns the account corresponding to the given address if the wallet
76+
// contains this account.
77+
func (w *Wallet) Unlock(address wallet.Address) (wallet.Account, error) {
78+
if _, ok := address.(*ethwallet.Address); !ok {
79+
return nil, errors.New("address must be ethwallet.Address")
80+
}
81+
82+
if acc, ok := w.Accounts[ethwallet.AsEthAddr(address)]; ok {
83+
return acc, nil
84+
}
85+
return nil, errors.New("account not found in wallet")
86+
}
87+
88+
// LockAll is called by the framework when a Client shuts down.
89+
func (w *Wallet) LockAll() {}
90+
91+
// IncrementUsage is called whenever a new channel is created or restored.
92+
func (w *Wallet) IncrementUsage(address wallet.Address) {}
93+
94+
// DecrementUsage is called whenever a channel is settled.
95+
func (w *Wallet) DecrementUsage(address wallet.Address) {}

0 commit comments

Comments
 (0)