mirror of
https://github.com/arnaucube/go-ethereum.git
synced 2026-03-04 08:04:50 +01:00
Compare commits
45 Commits
v1.5.8
...
release/1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a07539fb88 | ||
|
|
6988e5f074 | ||
|
|
aba016da72 | ||
|
|
09aef5c0ae | ||
|
|
9b161187ec | ||
|
|
8883f36fe3 | ||
|
|
0850f68fd1 | ||
|
|
57f4e90257 | ||
|
|
f8f428cc18 | ||
|
|
e23e86921b | ||
|
|
65ed6a9def | ||
|
|
e99c788155 | ||
|
|
c7022c1a0c | ||
|
|
26cd41f0c7 | ||
|
|
fb19846855 | ||
|
|
205ea95802 | ||
|
|
c5215fdd48 | ||
|
|
fad5eb0a87 | ||
|
|
b3c0e9d3cc | ||
|
|
470b79385b | ||
|
|
1ecf99bd0f | ||
|
|
e0fb4d1da9 | ||
|
|
ac2a0e615b | ||
|
|
52bd4e29ff | ||
|
|
833e4d1319 | ||
|
|
564b60520c | ||
|
|
085987ff2c | ||
|
|
aaf9cfd18c | ||
|
|
7ff686d6ec | ||
|
|
0cc9409fda | ||
|
|
6dd27e7cff | ||
|
|
d0eeb3ebdc | ||
|
|
fa99986143 | ||
|
|
6ea8eba8ce | ||
|
|
9b5c7153c9 | ||
|
|
d52b0c32a0 | ||
|
|
7734ead520 | ||
|
|
1bed9b3fea | ||
|
|
8b57c49490 | ||
|
|
296450451b | ||
|
|
4f5f90222f | ||
|
|
a2b4abd89a | ||
|
|
6d5e100d0d | ||
|
|
9b62facdd4 | ||
|
|
da92f5b2d6 |
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
.git
|
||||
build/_workspace
|
||||
build/_bin
|
||||
51
.travis.yml
51
.travis.yml
@@ -53,27 +53,52 @@ matrix:
|
||||
- CC=aarch64-linux-gnu-gcc go run build/ci.go install -arch arm64
|
||||
- go run build/ci.go archive -arch arm64 -type tar -signer LINUX_SIGNING_KEY -upload gethstore/builds
|
||||
|
||||
# This builder does the OSX Azure, Android Maven and Azure and iOS CocoaPods and Azure uploads
|
||||
# This builder does the Android Maven and Azure uploads
|
||||
- os: linux
|
||||
dist: precise # Needed for the android tools
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- oracle-java8-installer
|
||||
- oracle-java8-set-default
|
||||
language: android
|
||||
android:
|
||||
components:
|
||||
- platform-tools
|
||||
- tools
|
||||
- android-15
|
||||
- android-19
|
||||
- android-24
|
||||
env:
|
||||
- azure-android
|
||||
- maven-android
|
||||
before_install:
|
||||
- curl https://storage.googleapis.com/golang/go1.8rc3.linux-amd64.tar.gz | tar -xz
|
||||
- export PATH=`pwd`/go/bin:$PATH
|
||||
- export GOROOT=`pwd`/go
|
||||
- export GOPATH=$HOME/go # Drop post Go 1.8
|
||||
script:
|
||||
# Build the Android archive and upload it to Maven Central and Azure
|
||||
- curl https://dl.google.com/android/repository/android-ndk-r13b-linux-x86_64.zip -o android-ndk-r13b.zip
|
||||
- unzip -q android-ndk-r13b.zip && rm android-ndk-r13b.zip
|
||||
- mv android-ndk-r13b $HOME
|
||||
- export ANDROID_NDK=$HOME/android-ndk-r13b
|
||||
|
||||
- mkdir -p $GOPATH/src/github.com/ethereum
|
||||
- ln -s `pwd` $GOPATH/src/github.com/ethereum
|
||||
- go run build/ci.go aar -signer ANDROID_SIGNING_KEY -deploy https://oss.sonatype.org -upload gethstore/builds
|
||||
|
||||
# This builder does the OSX Azure, iOS CocoaPods and iOS Azure uploads
|
||||
- os: osx
|
||||
go: 1.7.4
|
||||
env:
|
||||
- azure-osx
|
||||
- mobile
|
||||
- azure-ios
|
||||
- cocoapods-ios
|
||||
script:
|
||||
- go run build/ci.go install
|
||||
- go run build/ci.go archive -type tar -signer OSX_SIGNING_KEY -upload gethstore/builds
|
||||
|
||||
# Build the Android archive and upload it to Maven Central and Azure
|
||||
- brew update
|
||||
- travis_wait 60 brew install android-sdk android-ndk maven gpg
|
||||
- alias gpg="gpg2"
|
||||
|
||||
- export ANDROID_HOME=/usr/local/opt/android-sdk
|
||||
- export ANDROID_NDK=/usr/local/opt/android-ndk
|
||||
- echo "y" | android update sdk --no-ui --filter `android list sdk | grep "SDK Platform Android" | grep -E 'API 15|API 19|API 24' | awk '{print $1}' | cut -d '-' -f 1 | tr '\n' ','`
|
||||
|
||||
- go run build/ci.go aar -signer ANDROID_SIGNING_KEY -deploy https://oss.sonatype.org -upload gethstore/builds
|
||||
|
||||
# Build the iOS framework and upload it to CocoaPods and Azure
|
||||
- gem uninstall cocoapods -a
|
||||
- gem install cocoapods --pre
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -1,11 +1,11 @@
|
||||
FROM alpine:3.3
|
||||
FROM alpine:3.5
|
||||
|
||||
ADD . /go-ethereum
|
||||
RUN \
|
||||
apk add --update git go make gcc musl-dev && \
|
||||
(cd go-ethereum && make geth) && \
|
||||
cp go-ethereum/build/bin/geth /geth && \
|
||||
apk del git go make gcc musl-dev && \
|
||||
apk add --update git go make gcc musl-dev linux-headers && \
|
||||
(cd go-ethereum && make geth) && \
|
||||
cp go-ethereum/build/bin/geth /geth && \
|
||||
apk del git go make gcc musl-dev linux-headers && \
|
||||
rm -rf /go-ethereum && rm -rf /var/cache/apk/*
|
||||
|
||||
EXPOSE 8545
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
@@ -35,7 +35,7 @@ func NewTransactor(keyin io.Reader, passphrase string) (*TransactOpts, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := accounts.DecryptKey(json, passphrase)
|
||||
key, err := keystore.DecryptKey(json, passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ func TestTypeRegexp(t *testing.T) {
|
||||
{"string[]", Type{IsSlice: true, SliceSize: -1, Kind: reflect.String, T: StringTy, Size: -1, Elem: &Type{Kind: reflect.String, T: StringTy, Size: -1, stringKind: "string"}, stringKind: "string[]"}},
|
||||
{"string[2]", Type{IsArray: true, SliceSize: 2, Kind: reflect.String, T: StringTy, Size: -1, Elem: &Type{Kind: reflect.String, T: StringTy, Size: -1, stringKind: "string"}, stringKind: "string[2]"}},
|
||||
{"address", Type{Kind: reflect.Array, Type: address_t, Size: 20, T: AddressTy, stringKind: "address"}},
|
||||
{"address[]", Type{IsSlice: true, SliceSize: -1,Kind: reflect.Array, Type:address_t, T: AddressTy, Size:20, Elem: &Type{Kind: reflect.Array, Type: address_t, Size: 20, T: AddressTy, stringKind: "address"}, stringKind: "address[]"}},
|
||||
{"address[2]", Type{IsArray: true, SliceSize: 2,Kind: reflect.Array, Type:address_t, T: AddressTy, Size:20, Elem: &Type{Kind: reflect.Array, Type: address_t, Size: 20, T: AddressTy, stringKind: "address"}, stringKind: "address[2]"}},
|
||||
{"address[]", Type{IsSlice: true, SliceSize: -1, Kind: reflect.Array, Type: address_t, T: AddressTy, Size: 20, Elem: &Type{Kind: reflect.Array, Type: address_t, Size: 20, T: AddressTy, stringKind: "address"}, stringKind: "address[]"}},
|
||||
{"address[2]", Type{IsArray: true, SliceSize: 2, Kind: reflect.Array, Type: address_t, T: AddressTy, Size: 20, Elem: &Type{Kind: reflect.Array, Type: address_t, Size: 20, T: AddressTy, stringKind: "address"}, stringKind: "address[2]"}},
|
||||
|
||||
// TODO when fixed types are implemented properly
|
||||
// {"fixed", Type{}},
|
||||
|
||||
@@ -1,350 +0,0 @@
|
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package accounts implements encrypted storage of secp256k1 private keys.
|
||||
//
|
||||
// Keys are stored as encrypted JSON files according to the Web3 Secret Storage specification.
|
||||
// See https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition for more information.
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
crand "crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrLocked = errors.New("account is locked")
|
||||
ErrNoMatch = errors.New("no key for given address or file")
|
||||
ErrDecrypt = errors.New("could not decrypt key with given passphrase")
|
||||
)
|
||||
|
||||
// Account represents a stored key.
|
||||
// When used as an argument, it selects a unique key file to act on.
|
||||
type Account struct {
|
||||
Address common.Address // Ethereum account address derived from the key
|
||||
|
||||
// File contains the key file name.
|
||||
// When Acccount is used as an argument to select a key, File can be left blank to
|
||||
// select just by address or set to the basename or absolute path of a file in the key
|
||||
// directory. Accounts returned by Manager will always contain an absolute path.
|
||||
File string
|
||||
}
|
||||
|
||||
func (acc *Account) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"` + acc.Address.Hex() + `"`), nil
|
||||
}
|
||||
|
||||
func (acc *Account) UnmarshalJSON(raw []byte) error {
|
||||
return json.Unmarshal(raw, &acc.Address)
|
||||
}
|
||||
|
||||
// Manager manages a key storage directory on disk.
|
||||
type Manager struct {
|
||||
cache *addrCache
|
||||
keyStore keyStore
|
||||
mu sync.RWMutex
|
||||
unlocked map[common.Address]*unlocked
|
||||
}
|
||||
|
||||
type unlocked struct {
|
||||
*Key
|
||||
abort chan struct{}
|
||||
}
|
||||
|
||||
// NewManager creates a manager for the given directory.
|
||||
func NewManager(keydir string, scryptN, scryptP int) *Manager {
|
||||
keydir, _ = filepath.Abs(keydir)
|
||||
am := &Manager{keyStore: &keyStorePassphrase{keydir, scryptN, scryptP}}
|
||||
am.init(keydir)
|
||||
return am
|
||||
}
|
||||
|
||||
// NewPlaintextManager creates a manager for the given directory.
|
||||
// Deprecated: Use NewManager.
|
||||
func NewPlaintextManager(keydir string) *Manager {
|
||||
keydir, _ = filepath.Abs(keydir)
|
||||
am := &Manager{keyStore: &keyStorePlain{keydir}}
|
||||
am.init(keydir)
|
||||
return am
|
||||
}
|
||||
|
||||
func (am *Manager) init(keydir string) {
|
||||
am.unlocked = make(map[common.Address]*unlocked)
|
||||
am.cache = newAddrCache(keydir)
|
||||
// TODO: In order for this finalizer to work, there must be no references
|
||||
// to am. addrCache doesn't keep a reference but unlocked keys do,
|
||||
// so the finalizer will not trigger until all timed unlocks have expired.
|
||||
runtime.SetFinalizer(am, func(m *Manager) {
|
||||
m.cache.close()
|
||||
})
|
||||
}
|
||||
|
||||
// HasAddress reports whether a key with the given address is present.
|
||||
func (am *Manager) HasAddress(addr common.Address) bool {
|
||||
return am.cache.hasAddress(addr)
|
||||
}
|
||||
|
||||
// Accounts returns all key files present in the directory.
|
||||
func (am *Manager) Accounts() []Account {
|
||||
return am.cache.accounts()
|
||||
}
|
||||
|
||||
// Delete deletes the key matched by account if the passphrase is correct.
|
||||
// If the account contains no filename, the address must match a unique key.
|
||||
func (am *Manager) Delete(a Account, passphrase string) error {
|
||||
// Decrypting the key isn't really necessary, but we do
|
||||
// it anyway to check the password and zero out the key
|
||||
// immediately afterwards.
|
||||
a, key, err := am.getDecryptedKey(a, passphrase)
|
||||
if key != nil {
|
||||
zeroKey(key.PrivateKey)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The order is crucial here. The key is dropped from the
|
||||
// cache after the file is gone so that a reload happening in
|
||||
// between won't insert it into the cache again.
|
||||
err = os.Remove(a.File)
|
||||
if err == nil {
|
||||
am.cache.delete(a)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Sign calculates a ECDSA signature for the given hash. The produced signature
|
||||
// is in the [R || S || V] format where V is 0 or 1.
|
||||
func (am *Manager) Sign(addr common.Address, hash []byte) ([]byte, error) {
|
||||
am.mu.RLock()
|
||||
defer am.mu.RUnlock()
|
||||
|
||||
unlockedKey, found := am.unlocked[addr]
|
||||
if !found {
|
||||
return nil, ErrLocked
|
||||
}
|
||||
return crypto.Sign(hash, unlockedKey.PrivateKey)
|
||||
}
|
||||
|
||||
// SignWithPassphrase signs hash if the private key matching the given address
|
||||
// can be decrypted with the given passphrase. The produced signature is in the
|
||||
// [R || S || V] format where V is 0 or 1.
|
||||
func (am *Manager) SignWithPassphrase(a Account, passphrase string, hash []byte) (signature []byte, err error) {
|
||||
_, key, err := am.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer zeroKey(key.PrivateKey)
|
||||
return crypto.Sign(hash, key.PrivateKey)
|
||||
}
|
||||
|
||||
// Unlock unlocks the given account indefinitely.
|
||||
func (am *Manager) Unlock(a Account, passphrase string) error {
|
||||
return am.TimedUnlock(a, passphrase, 0)
|
||||
}
|
||||
|
||||
// Lock removes the private key with the given address from memory.
|
||||
func (am *Manager) Lock(addr common.Address) error {
|
||||
am.mu.Lock()
|
||||
if unl, found := am.unlocked[addr]; found {
|
||||
am.mu.Unlock()
|
||||
am.expire(addr, unl, time.Duration(0)*time.Nanosecond)
|
||||
} else {
|
||||
am.mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TimedUnlock unlocks the given account with the passphrase. The account
|
||||
// stays unlocked for the duration of timeout. A timeout of 0 unlocks the account
|
||||
// until the program exits. The account must match a unique key file.
|
||||
//
|
||||
// If the account address is already unlocked for a duration, TimedUnlock extends or
|
||||
// shortens the active unlock timeout. If the address was previously unlocked
|
||||
// indefinitely the timeout is not altered.
|
||||
func (am *Manager) TimedUnlock(a Account, passphrase string, timeout time.Duration) error {
|
||||
a, key, err := am.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
am.mu.Lock()
|
||||
defer am.mu.Unlock()
|
||||
u, found := am.unlocked[a.Address]
|
||||
if found {
|
||||
if u.abort == nil {
|
||||
// The address was unlocked indefinitely, so unlocking
|
||||
// it with a timeout would be confusing.
|
||||
zeroKey(key.PrivateKey)
|
||||
return nil
|
||||
} else {
|
||||
// Terminate the expire goroutine and replace it below.
|
||||
close(u.abort)
|
||||
}
|
||||
}
|
||||
if timeout > 0 {
|
||||
u = &unlocked{Key: key, abort: make(chan struct{})}
|
||||
go am.expire(a.Address, u, timeout)
|
||||
} else {
|
||||
u = &unlocked{Key: key}
|
||||
}
|
||||
am.unlocked[a.Address] = u
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find resolves the given account into a unique entry in the keystore.
|
||||
func (am *Manager) Find(a Account) (Account, error) {
|
||||
am.cache.maybeReload()
|
||||
am.cache.mu.Lock()
|
||||
a, err := am.cache.find(a)
|
||||
am.cache.mu.Unlock()
|
||||
return a, err
|
||||
}
|
||||
|
||||
func (am *Manager) getDecryptedKey(a Account, auth string) (Account, *Key, error) {
|
||||
a, err := am.Find(a)
|
||||
if err != nil {
|
||||
return a, nil, err
|
||||
}
|
||||
key, err := am.keyStore.GetKey(a.Address, a.File, auth)
|
||||
return a, key, err
|
||||
}
|
||||
|
||||
func (am *Manager) expire(addr common.Address, u *unlocked, timeout time.Duration) {
|
||||
t := time.NewTimer(timeout)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case <-u.abort:
|
||||
// just quit
|
||||
case <-t.C:
|
||||
am.mu.Lock()
|
||||
// only drop if it's still the same key instance that dropLater
|
||||
// was launched with. we can check that using pointer equality
|
||||
// because the map stores a new pointer every time the key is
|
||||
// unlocked.
|
||||
if am.unlocked[addr] == u {
|
||||
zeroKey(u.PrivateKey)
|
||||
delete(am.unlocked, addr)
|
||||
}
|
||||
am.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// NewAccount generates a new key and stores it into the key directory,
|
||||
// encrypting it with the passphrase.
|
||||
func (am *Manager) NewAccount(passphrase string) (Account, error) {
|
||||
_, account, err := storeNewKey(am.keyStore, crand.Reader, passphrase)
|
||||
if err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
// Add the account to the cache immediately rather
|
||||
// than waiting for file system notifications to pick it up.
|
||||
am.cache.add(account)
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// AccountByIndex returns the ith account.
|
||||
func (am *Manager) AccountByIndex(i int) (Account, error) {
|
||||
accounts := am.Accounts()
|
||||
if i < 0 || i >= len(accounts) {
|
||||
return Account{}, fmt.Errorf("account index %d out of range [0, %d]", i, len(accounts)-1)
|
||||
}
|
||||
return accounts[i], nil
|
||||
}
|
||||
|
||||
// Export exports as a JSON key, encrypted with newPassphrase.
|
||||
func (am *Manager) Export(a Account, passphrase, newPassphrase string) (keyJSON []byte, err error) {
|
||||
_, key, err := am.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var N, P int
|
||||
if store, ok := am.keyStore.(*keyStorePassphrase); ok {
|
||||
N, P = store.scryptN, store.scryptP
|
||||
} else {
|
||||
N, P = StandardScryptN, StandardScryptP
|
||||
}
|
||||
return EncryptKey(key, newPassphrase, N, P)
|
||||
}
|
||||
|
||||
// Import stores the given encrypted JSON key into the key directory.
|
||||
func (am *Manager) Import(keyJSON []byte, passphrase, newPassphrase string) (Account, error) {
|
||||
key, err := DecryptKey(keyJSON, passphrase)
|
||||
if key != nil && key.PrivateKey != nil {
|
||||
defer zeroKey(key.PrivateKey)
|
||||
}
|
||||
if err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
return am.importKey(key, newPassphrase)
|
||||
}
|
||||
|
||||
// ImportECDSA stores the given key into the key directory, encrypting it with the passphrase.
|
||||
func (am *Manager) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (Account, error) {
|
||||
key := newKeyFromECDSA(priv)
|
||||
if am.cache.hasAddress(key.Address) {
|
||||
return Account{}, fmt.Errorf("account already exists")
|
||||
}
|
||||
|
||||
return am.importKey(key, passphrase)
|
||||
}
|
||||
|
||||
func (am *Manager) importKey(key *Key, passphrase string) (Account, error) {
|
||||
a := Account{Address: key.Address, File: am.keyStore.JoinPath(keyFileName(key.Address))}
|
||||
if err := am.keyStore.StoreKey(a.File, key, passphrase); err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
am.cache.add(a)
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Update changes the passphrase of an existing account.
|
||||
func (am *Manager) Update(a Account, passphrase, newPassphrase string) error {
|
||||
a, key, err := am.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return am.keyStore.StoreKey(a.File, key, newPassphrase)
|
||||
}
|
||||
|
||||
// ImportPreSaleKey decrypts the given Ethereum presale wallet and stores
|
||||
// a key file in the key directory. The key file is encrypted with the same passphrase.
|
||||
func (am *Manager) ImportPreSaleKey(keyJSON []byte, passphrase string) (Account, error) {
|
||||
a, _, err := importPreSaleKey(am.keyStore, keyJSON, passphrase)
|
||||
if err != nil {
|
||||
return a, err
|
||||
}
|
||||
am.cache.add(a)
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// zeroKey zeroes a private key in memory.
|
||||
func zeroKey(k *ecdsa.PrivateKey) {
|
||||
b := k.D.Bits()
|
||||
for i := range b {
|
||||
b[i] = 0
|
||||
}
|
||||
}
|
||||
155
accounts/accounts.go
Normal file
155
accounts/accounts.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package accounts implements high level Ethereum account management.
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
ethereum "github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
)
|
||||
|
||||
// Account represents an Ethereum account located at a specific location defined
|
||||
// by the optional URL field.
|
||||
type Account struct {
|
||||
Address common.Address `json:"address"` // Ethereum account address derived from the key
|
||||
URL URL `json:"url"` // Optional resource locator within a backend
|
||||
}
|
||||
|
||||
// Wallet represents a software or hardware wallet that might contain one or more
|
||||
// accounts (derived from the same seed).
|
||||
type Wallet interface {
|
||||
// URL retrieves the canonical path under which this wallet is reachable. It is
|
||||
// user by upper layers to define a sorting order over all wallets from multiple
|
||||
// backends.
|
||||
URL() URL
|
||||
|
||||
// Status returns a textual status to aid the user in the current state of the
|
||||
// wallet.
|
||||
Status() string
|
||||
|
||||
// Open initializes access to a wallet instance. It is not meant to unlock or
|
||||
// decrypt account keys, rather simply to establish a connection to hardware
|
||||
// wallets and/or to access derivation seeds.
|
||||
//
|
||||
// The passphrase parameter may or may not be used by the implementation of a
|
||||
// particular wallet instance. The reason there is no passwordless open method
|
||||
// is to strive towards a uniform wallet handling, oblivious to the different
|
||||
// backend providers.
|
||||
//
|
||||
// Please note, if you open a wallet, you must close it to release any allocated
|
||||
// resources (especially important when working with hardware wallets).
|
||||
Open(passphrase string) error
|
||||
|
||||
// Close releases any resources held by an open wallet instance.
|
||||
Close() error
|
||||
|
||||
// Accounts retrieves the list of signing accounts the wallet is currently aware
|
||||
// of. For hierarchical deterministic wallets, the list will not be exhaustive,
|
||||
// rather only contain the accounts explicitly pinned during account derivation.
|
||||
Accounts() []Account
|
||||
|
||||
// Contains returns whether an account is part of this particular wallet or not.
|
||||
Contains(account Account) bool
|
||||
|
||||
// Derive attempts to explicitly derive a hierarchical deterministic account at
|
||||
// the specified derivation path. If requested, the derived account will be added
|
||||
// to the wallet's tracked account list.
|
||||
Derive(path DerivationPath, pin bool) (Account, error)
|
||||
|
||||
// SelfDerive sets a base account derivation path from which the wallet attempts
|
||||
// to discover non zero accounts and automatically add them to list of tracked
|
||||
// accounts.
|
||||
//
|
||||
// Note, self derivaton will increment the last component of the specified path
|
||||
// opposed to decending into a child path to allow discovering accounts starting
|
||||
// from non zero components.
|
||||
//
|
||||
// You can disable automatic account discovery by calling SelfDerive with a nil
|
||||
// chain state reader.
|
||||
SelfDerive(base DerivationPath, chain ethereum.ChainStateReader)
|
||||
|
||||
// SignHash requests the wallet to sign the given hash.
|
||||
//
|
||||
// It looks up the account specified either solely via its address contained within,
|
||||
// or optionally with the aid of any location metadata from the embedded URL field.
|
||||
//
|
||||
// If the wallet requires additional authentication to sign the request (e.g.
|
||||
// a password to decrypt the account, or a PIN code o verify the transaction),
|
||||
// an AuthNeededError instance will be returned, containing infos for the user
|
||||
// about which fields or actions are needed. The user may retry by providing
|
||||
// the needed details via SignHashWithPassphrase, or by other means (e.g. unlock
|
||||
// the account in a keystore).
|
||||
SignHash(account Account, hash []byte) ([]byte, error)
|
||||
|
||||
// SignTx requests the wallet to sign the given transaction.
|
||||
//
|
||||
// It looks up the account specified either solely via its address contained within,
|
||||
// or optionally with the aid of any location metadata from the embedded URL field.
|
||||
//
|
||||
// If the wallet requires additional authentication to sign the request (e.g.
|
||||
// a password to decrypt the account, or a PIN code o verify the transaction),
|
||||
// an AuthNeededError instance will be returned, containing infos for the user
|
||||
// about which fields or actions are needed. The user may retry by providing
|
||||
// the needed details via SignTxWithPassphrase, or by other means (e.g. unlock
|
||||
// the account in a keystore).
|
||||
SignTx(account Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
|
||||
|
||||
// SignHashWithPassphrase requests the wallet to sign the given hash with the
|
||||
// given passphrase as extra authentication information.
|
||||
//
|
||||
// It looks up the account specified either solely via its address contained within,
|
||||
// or optionally with the aid of any location metadata from the embedded URL field.
|
||||
SignHashWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error)
|
||||
|
||||
// SignTxWithPassphrase requests the wallet to sign the given transaction, with the
|
||||
// given passphrase as extra authentication information.
|
||||
//
|
||||
// It looks up the account specified either solely via its address contained within,
|
||||
// or optionally with the aid of any location metadata from the embedded URL field.
|
||||
SignTxWithPassphrase(account Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
|
||||
}
|
||||
|
||||
// Backend is a "wallet provider" that may contain a batch of accounts they can
|
||||
// sign transactions with and upon request, do so.
|
||||
type Backend interface {
|
||||
// Wallets retrieves the list of wallets the backend is currently aware of.
|
||||
//
|
||||
// The returned wallets are not opened by default. For software HD wallets this
|
||||
// means that no base seeds are decrypted, and for hardware wallets that no actual
|
||||
// connection is established.
|
||||
//
|
||||
// The resulting wallet list will be sorted alphabetically based on its internal
|
||||
// URL assigned by the backend. Since wallets (especially hardware) may come and
|
||||
// go, the same wallet might appear at a different positions in the list during
|
||||
// subsequent retrievals.
|
||||
Wallets() []Wallet
|
||||
|
||||
// Subscribe creates an async subscription to receive notifications when the
|
||||
// backend detects the arrival or departure of a wallet.
|
||||
Subscribe(sink chan<- WalletEvent) event.Subscription
|
||||
}
|
||||
|
||||
// WalletEvent is an event fired by an account backend when a wallet arrival or
|
||||
// departure is detected.
|
||||
type WalletEvent struct {
|
||||
Wallet Wallet // Wallet instance arrived or departed
|
||||
Arrive bool // Whether the wallet was added or removed
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
var testSigData = make([]byte, 32)
|
||||
|
||||
func TestManager(t *testing.T) {
|
||||
dir, am := tmpManager(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
a, err := am.NewAccount("foo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(a.File, dir) {
|
||||
t.Errorf("account file %s doesn't have dir prefix", a.File)
|
||||
}
|
||||
stat, err := os.Stat(a.File)
|
||||
if err != nil {
|
||||
t.Fatalf("account file %s doesn't exist (%v)", a.File, err)
|
||||
}
|
||||
if runtime.GOOS != "windows" && stat.Mode() != 0600 {
|
||||
t.Fatalf("account file has wrong mode: got %o, want %o", stat.Mode(), 0600)
|
||||
}
|
||||
if !am.HasAddress(a.Address) {
|
||||
t.Errorf("HasAccount(%x) should've returned true", a.Address)
|
||||
}
|
||||
if err := am.Update(a, "foo", "bar"); err != nil {
|
||||
t.Errorf("Update error: %v", err)
|
||||
}
|
||||
if err := am.Delete(a, "bar"); err != nil {
|
||||
t.Errorf("Delete error: %v", err)
|
||||
}
|
||||
if common.FileExist(a.File) {
|
||||
t.Errorf("account file %s should be gone after Delete", a.File)
|
||||
}
|
||||
if am.HasAddress(a.Address) {
|
||||
t.Errorf("HasAccount(%x) should've returned true after Delete", a.Address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
dir, am := tmpManager(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "" // not used but required by API
|
||||
a1, err := am.NewAccount(pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := am.Unlock(a1, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := am.Sign(a1.Address, testSigData); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithPassphrase(t *testing.T) {
|
||||
dir, am := tmpManager(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "passwd"
|
||||
acc, err := am.NewAccount(pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, unlocked := am.unlocked[acc.Address]; unlocked {
|
||||
t.Fatal("expected account to be locked")
|
||||
}
|
||||
|
||||
_, err = am.SignWithPassphrase(acc, pass, testSigData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, unlocked := am.unlocked[acc.Address]; unlocked {
|
||||
t.Fatal("expected account to be locked")
|
||||
}
|
||||
|
||||
if _, err = am.SignWithPassphrase(acc, "invalid passwd", testSigData); err == nil {
|
||||
t.Fatal("expected SignHash to fail with invalid password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimedUnlock(t *testing.T) {
|
||||
dir, am := tmpManager(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "foo"
|
||||
a1, err := am.NewAccount(pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Signing without passphrase fails because account is locked
|
||||
_, err = am.Sign(a1.Address, testSigData)
|
||||
if err != ErrLocked {
|
||||
t.Fatal("Signing should've failed with ErrLocked before unlocking, got ", err)
|
||||
}
|
||||
|
||||
// Signing with passphrase works
|
||||
if err = am.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Signing without passphrase works because account is temp unlocked
|
||||
_, err = am.Sign(a1.Address, testSigData)
|
||||
if err != nil {
|
||||
t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
|
||||
}
|
||||
|
||||
// Signing fails again after automatic locking
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
_, err = am.Sign(a1.Address, testSigData)
|
||||
if err != ErrLocked {
|
||||
t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverrideUnlock(t *testing.T) {
|
||||
dir, am := tmpManager(t, false)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "foo"
|
||||
a1, err := am.NewAccount(pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Unlock indefinitely.
|
||||
if err = am.TimedUnlock(a1, pass, 5*time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Signing without passphrase works because account is temp unlocked
|
||||
_, err = am.Sign(a1.Address, testSigData)
|
||||
if err != nil {
|
||||
t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
|
||||
}
|
||||
|
||||
// reset unlock to a shorter period, invalidates the previous unlock
|
||||
if err = am.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Signing without passphrase still works because account is temp unlocked
|
||||
_, err = am.Sign(a1.Address, testSigData)
|
||||
if err != nil {
|
||||
t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
|
||||
}
|
||||
|
||||
// Signing fails again after automatic locking
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
_, err = am.Sign(a1.Address, testSigData)
|
||||
if err != ErrLocked {
|
||||
t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// This test should fail under -race if signing races the expiration goroutine.
|
||||
func TestSignRace(t *testing.T) {
|
||||
dir, am := tmpManager(t, false)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// Create a test account.
|
||||
a1, err := am.NewAccount("")
|
||||
if err != nil {
|
||||
t.Fatal("could not create the test account", err)
|
||||
}
|
||||
|
||||
if err := am.TimedUnlock(a1, "", 15*time.Millisecond); err != nil {
|
||||
t.Fatal("could not unlock the test account", err)
|
||||
}
|
||||
end := time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(end) {
|
||||
if _, err := am.Sign(a1.Address, testSigData); err == ErrLocked {
|
||||
return
|
||||
} else if err != nil {
|
||||
t.Errorf("Sign error: %v", err)
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
t.Errorf("Account did not lock within the timeout")
|
||||
}
|
||||
|
||||
func tmpManager(t *testing.T, encrypted bool) (string, *Manager) {
|
||||
d, err := ioutil.TempDir("", "eth-keystore-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
new := NewPlaintextManager
|
||||
if encrypted {
|
||||
new = func(kd string) *Manager { return NewManager(kd, veryLightScryptN, veryLightScryptP) }
|
||||
}
|
||||
return d, new(d)
|
||||
}
|
||||
68
accounts/errors.go
Normal file
68
accounts/errors.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrUnknownAccount is returned for any requested operation for which no backend
|
||||
// provides the specified account.
|
||||
var ErrUnknownAccount = errors.New("unknown account")
|
||||
|
||||
// ErrUnknownWallet is returned for any requested operation for which no backend
|
||||
// provides the specified wallet.
|
||||
var ErrUnknownWallet = errors.New("unknown wallet")
|
||||
|
||||
// ErrNotSupported is returned when an operation is requested from an account
|
||||
// backend that it does not support.
|
||||
var ErrNotSupported = errors.New("not supported")
|
||||
|
||||
// ErrInvalidPassphrase is returned when a decryption operation receives a bad
|
||||
// passphrase.
|
||||
var ErrInvalidPassphrase = errors.New("invalid passphrase")
|
||||
|
||||
// ErrWalletAlreadyOpen is returned if a wallet is attempted to be opened the
|
||||
// secodn time.
|
||||
var ErrWalletAlreadyOpen = errors.New("wallet already open")
|
||||
|
||||
// ErrWalletClosed is returned if a wallet is attempted to be opened the
|
||||
// secodn time.
|
||||
var ErrWalletClosed = errors.New("wallet closed")
|
||||
|
||||
// AuthNeededError is returned by backends for signing requests where the user
|
||||
// is required to provide further authentication before signing can succeed.
|
||||
//
|
||||
// This usually means either that a password needs to be supplied, or perhaps a
|
||||
// one time PIN code displayed by some hardware device.
|
||||
type AuthNeededError struct {
|
||||
Needed string // Extra authentication the user needs to provide
|
||||
}
|
||||
|
||||
// NewAuthNeededError creates a new authentication error with the extra details
|
||||
// about the needed fields set.
|
||||
func NewAuthNeededError(needed string) error {
|
||||
return &AuthNeededError{
|
||||
Needed: needed,
|
||||
}
|
||||
}
|
||||
|
||||
// Error implements the standard error interfacel.
|
||||
func (err *AuthNeededError) Error() string {
|
||||
return fmt.Sprintf("authentication needed: %s", err.Needed)
|
||||
}
|
||||
130
accounts/hd.go
Normal file
130
accounts/hd.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DefaultRootDerivationPath is the root path to which custom derivation endpoints
|
||||
// are appended. As such, the first account will be at m/44'/60'/0'/0, the second
|
||||
// at m/44'/60'/0'/1, etc.
|
||||
var DefaultRootDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0}
|
||||
|
||||
// DefaultBaseDerivationPath is the base path from which custom derivation endpoints
|
||||
// are incremented. As such, the first account will be at m/44'/60'/0'/0, the second
|
||||
// at m/44'/60'/0'/1, etc.
|
||||
var DefaultBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}
|
||||
|
||||
// DerivationPath represents the computer friendly version of a hierarchical
|
||||
// deterministic wallet account derivaion path.
|
||||
//
|
||||
// The BIP-32 spec https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
|
||||
// defines derivation paths to be of the form:
|
||||
//
|
||||
// m / purpose' / coin_type' / account' / change / address_index
|
||||
//
|
||||
// The BIP-44 spec https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
|
||||
// defines that the `purpose` be 44' (or 0x8000002C) for crypto currencies, and
|
||||
// SLIP-44 https://github.com/satoshilabs/slips/blob/master/slip-0044.md assigns
|
||||
// the `coin_type` 60' (or 0x8000003C) to Ethereum.
|
||||
//
|
||||
// The root path for Ethereum is m/44'/60'/0'/0 according to the specification
|
||||
// from https://github.com/ethereum/EIPs/issues/84, albeit it's not set in stone
|
||||
// yet whether accounts should increment the last component or the children of
|
||||
// that. We will go with the simpler approach of incrementing the last component.
|
||||
type DerivationPath []uint32
|
||||
|
||||
// ParseDerivationPath converts a user specified derivation path string to the
|
||||
// internal binary representation.
|
||||
//
|
||||
// Full derivation paths need to start with the `m/` prefix, relative derivation
|
||||
// paths (which will get appended to the default root path) must not have prefixes
|
||||
// in front of the first element. Whitespace is ignored.
|
||||
func ParseDerivationPath(path string) (DerivationPath, error) {
|
||||
var result DerivationPath
|
||||
|
||||
// Handle absolute or relative paths
|
||||
components := strings.Split(path, "/")
|
||||
switch {
|
||||
case len(components) == 0:
|
||||
return nil, errors.New("empty derivation path")
|
||||
|
||||
case strings.TrimSpace(components[0]) == "":
|
||||
return nil, errors.New("ambiguous path: use 'm/' prefix for absolute paths, or no leading '/' for relative ones")
|
||||
|
||||
case strings.TrimSpace(components[0]) == "m":
|
||||
components = components[1:]
|
||||
|
||||
default:
|
||||
result = append(result, DefaultRootDerivationPath...)
|
||||
}
|
||||
// All remaining components are relative, append one by one
|
||||
if len(components) == 0 {
|
||||
return nil, errors.New("empty derivation path") // Empty relative paths
|
||||
}
|
||||
for _, component := range components {
|
||||
// Ignore any user added whitespace
|
||||
component = strings.TrimSpace(component)
|
||||
var value uint32
|
||||
|
||||
// Handle hardened paths
|
||||
if strings.HasSuffix(component, "'") {
|
||||
value = 0x80000000
|
||||
component = strings.TrimSpace(strings.TrimSuffix(component, "'"))
|
||||
}
|
||||
// Handle the non hardened component
|
||||
bigval, ok := new(big.Int).SetString(component, 0)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid component: %s", component)
|
||||
}
|
||||
max := math.MaxUint32 - value
|
||||
if bigval.Sign() < 0 || bigval.Cmp(big.NewInt(int64(max))) > 0 {
|
||||
if value == 0 {
|
||||
return nil, fmt.Errorf("component %v out of allowed range [0, %d]", bigval, max)
|
||||
}
|
||||
return nil, fmt.Errorf("component %v out of allowed hardened range [0, %d]", bigval, max)
|
||||
}
|
||||
value += uint32(bigval.Uint64())
|
||||
|
||||
// Append and repeat
|
||||
result = append(result, value)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// String implements the stringer interface, converting a binary derivation path
|
||||
// to its canonical representation.
|
||||
func (path DerivationPath) String() string {
|
||||
result := "m"
|
||||
for _, component := range path {
|
||||
var hardened bool
|
||||
if component >= 0x80000000 {
|
||||
component -= 0x80000000
|
||||
hardened = true
|
||||
}
|
||||
result = fmt.Sprintf("%s/%d", result, component)
|
||||
if hardened {
|
||||
result += "'"
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
79
accounts/hd_test.go
Normal file
79
accounts/hd_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests that HD derivation paths can be correctly parsed into our internal binary
|
||||
// representation.
|
||||
func TestHDPathParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
output DerivationPath
|
||||
}{
|
||||
// Plain absolute derivation paths
|
||||
{"m/44'/60'/0'/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||
{"m/44'/60'/0'/128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
|
||||
{"m/44'/60'/0'/0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||
{"m/44'/60'/0'/128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
|
||||
{"m/2147483692/2147483708/2147483648/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||
{"m/2147483692/2147483708/2147483648/2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||
|
||||
// Plain relative derivation paths
|
||||
{"0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||
{"128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
|
||||
{"0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||
{"128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
|
||||
{"2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||
|
||||
// Hexadecimal absolute derivation paths
|
||||
{"m/0x2C'/0x3c'/0x00'/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||
{"m/0x2C'/0x3c'/0x00'/0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
|
||||
{"m/0x2C'/0x3c'/0x00'/0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||
{"m/0x2C'/0x3c'/0x00'/0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
|
||||
{"m/0x8000002C/0x8000003c/0x80000000/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||
{"m/0x8000002C/0x8000003c/0x80000000/0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||
|
||||
// Hexadecimal relative derivation paths
|
||||
{"0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||
{"0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
|
||||
{"0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||
{"0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
|
||||
{"0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
|
||||
|
||||
// Weird inputs just to ensure they work
|
||||
{" m / 44 '\n/\n 60 \n\n\t' /\n0 ' /\t\t 0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
|
||||
|
||||
// Invaid derivation paths
|
||||
{"", nil}, // Empty relative derivation path
|
||||
{"m", nil}, // Empty absolute derivation path
|
||||
{"m/", nil}, // Missing last derivation component
|
||||
{"/44'/60'/0'/0", nil}, // Absolute path without m prefix, might be user error
|
||||
{"m/2147483648'", nil}, // Overflows 32 bit integer
|
||||
{"m/-1'", nil}, // Cannot contain negative number
|
||||
}
|
||||
for i, tt := range tests {
|
||||
if path, err := ParseDerivationPath(tt.input); !reflect.DeepEqual(path, tt.output) {
|
||||
t.Errorf("test %d: parse mismatch: have %v (%v), want %v", i, path, err, tt.output)
|
||||
} else if path == nil && err == nil {
|
||||
t.Errorf("test %d: nil path and error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
@@ -38,23 +39,23 @@ import (
|
||||
// exist yet, the code will attempt to create a watcher at most this often.
|
||||
const minReloadInterval = 2 * time.Second
|
||||
|
||||
type accountsByFile []Account
|
||||
type accountsByURL []accounts.Account
|
||||
|
||||
func (s accountsByFile) Len() int { return len(s) }
|
||||
func (s accountsByFile) Less(i, j int) bool { return s[i].File < s[j].File }
|
||||
func (s accountsByFile) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s accountsByURL) Len() int { return len(s) }
|
||||
func (s accountsByURL) Less(i, j int) bool { return s[i].URL.Cmp(s[j].URL) < 0 }
|
||||
func (s accountsByURL) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
|
||||
// AmbiguousAddrError is returned when attempting to unlock
|
||||
// an address for which more than one file exists.
|
||||
type AmbiguousAddrError struct {
|
||||
Addr common.Address
|
||||
Matches []Account
|
||||
Matches []accounts.Account
|
||||
}
|
||||
|
||||
func (err *AmbiguousAddrError) Error() string {
|
||||
files := ""
|
||||
for i, a := range err.Matches {
|
||||
files += a.File
|
||||
files += a.URL.Path
|
||||
if i < len(err.Matches)-1 {
|
||||
files += ", "
|
||||
}
|
||||
@@ -62,60 +63,63 @@ func (err *AmbiguousAddrError) Error() string {
|
||||
return fmt.Sprintf("multiple keys match address (%s)", files)
|
||||
}
|
||||
|
||||
// addrCache is a live index of all accounts in the keystore.
|
||||
type addrCache struct {
|
||||
// accountCache is a live index of all accounts in the keystore.
|
||||
type accountCache struct {
|
||||
keydir string
|
||||
watcher *watcher
|
||||
mu sync.Mutex
|
||||
all accountsByFile
|
||||
byAddr map[common.Address][]Account
|
||||
all accountsByURL
|
||||
byAddr map[common.Address][]accounts.Account
|
||||
throttle *time.Timer
|
||||
notify chan struct{}
|
||||
}
|
||||
|
||||
func newAddrCache(keydir string) *addrCache {
|
||||
ac := &addrCache{
|
||||
func newAccountCache(keydir string) (*accountCache, chan struct{}) {
|
||||
ac := &accountCache{
|
||||
keydir: keydir,
|
||||
byAddr: make(map[common.Address][]Account),
|
||||
byAddr: make(map[common.Address][]accounts.Account),
|
||||
notify: make(chan struct{}, 1),
|
||||
}
|
||||
ac.watcher = newWatcher(ac)
|
||||
return ac
|
||||
return ac, ac.notify
|
||||
}
|
||||
|
||||
func (ac *addrCache) accounts() []Account {
|
||||
func (ac *accountCache) accounts() []accounts.Account {
|
||||
ac.maybeReload()
|
||||
ac.mu.Lock()
|
||||
defer ac.mu.Unlock()
|
||||
cpy := make([]Account, len(ac.all))
|
||||
cpy := make([]accounts.Account, len(ac.all))
|
||||
copy(cpy, ac.all)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (ac *addrCache) hasAddress(addr common.Address) bool {
|
||||
func (ac *accountCache) hasAddress(addr common.Address) bool {
|
||||
ac.maybeReload()
|
||||
ac.mu.Lock()
|
||||
defer ac.mu.Unlock()
|
||||
return len(ac.byAddr[addr]) > 0
|
||||
}
|
||||
|
||||
func (ac *addrCache) add(newAccount Account) {
|
||||
func (ac *accountCache) add(newAccount accounts.Account) {
|
||||
ac.mu.Lock()
|
||||
defer ac.mu.Unlock()
|
||||
|
||||
i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].File >= newAccount.File })
|
||||
i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Cmp(newAccount.URL) >= 0 })
|
||||
if i < len(ac.all) && ac.all[i] == newAccount {
|
||||
return
|
||||
}
|
||||
// newAccount is not in the cache.
|
||||
ac.all = append(ac.all, Account{})
|
||||
ac.all = append(ac.all, accounts.Account{})
|
||||
copy(ac.all[i+1:], ac.all[i:])
|
||||
ac.all[i] = newAccount
|
||||
ac.byAddr[newAccount.Address] = append(ac.byAddr[newAccount.Address], newAccount)
|
||||
}
|
||||
|
||||
// note: removed needs to be unique here (i.e. both File and Address must be set).
|
||||
func (ac *addrCache) delete(removed Account) {
|
||||
func (ac *accountCache) delete(removed accounts.Account) {
|
||||
ac.mu.Lock()
|
||||
defer ac.mu.Unlock()
|
||||
|
||||
ac.all = removeAccount(ac.all, removed)
|
||||
if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 {
|
||||
delete(ac.byAddr, removed.Address)
|
||||
@@ -124,7 +128,7 @@ func (ac *addrCache) delete(removed Account) {
|
||||
}
|
||||
}
|
||||
|
||||
func removeAccount(slice []Account, elem Account) []Account {
|
||||
func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.Account {
|
||||
for i := range slice {
|
||||
if slice[i] == elem {
|
||||
return append(slice[:i], slice[i+1:]...)
|
||||
@@ -134,43 +138,44 @@ func removeAccount(slice []Account, elem Account) []Account {
|
||||
}
|
||||
|
||||
// find returns the cached account for address if there is a unique match.
|
||||
// The exact matching rules are explained by the documentation of Account.
|
||||
// The exact matching rules are explained by the documentation of accounts.Account.
|
||||
// Callers must hold ac.mu.
|
||||
func (ac *addrCache) find(a Account) (Account, error) {
|
||||
func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) {
|
||||
// Limit search to address candidates if possible.
|
||||
matches := ac.all
|
||||
if (a.Address != common.Address{}) {
|
||||
matches = ac.byAddr[a.Address]
|
||||
}
|
||||
if a.File != "" {
|
||||
if a.URL.Path != "" {
|
||||
// If only the basename is specified, complete the path.
|
||||
if !strings.ContainsRune(a.File, filepath.Separator) {
|
||||
a.File = filepath.Join(ac.keydir, a.File)
|
||||
if !strings.ContainsRune(a.URL.Path, filepath.Separator) {
|
||||
a.URL.Path = filepath.Join(ac.keydir, a.URL.Path)
|
||||
}
|
||||
for i := range matches {
|
||||
if matches[i].File == a.File {
|
||||
if matches[i].URL == a.URL {
|
||||
return matches[i], nil
|
||||
}
|
||||
}
|
||||
if (a.Address == common.Address{}) {
|
||||
return Account{}, ErrNoMatch
|
||||
return accounts.Account{}, ErrNoMatch
|
||||
}
|
||||
}
|
||||
switch len(matches) {
|
||||
case 1:
|
||||
return matches[0], nil
|
||||
case 0:
|
||||
return Account{}, ErrNoMatch
|
||||
return accounts.Account{}, ErrNoMatch
|
||||
default:
|
||||
err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]Account, len(matches))}
|
||||
err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]accounts.Account, len(matches))}
|
||||
copy(err.Matches, matches)
|
||||
return Account{}, err
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
}
|
||||
|
||||
func (ac *addrCache) maybeReload() {
|
||||
func (ac *accountCache) maybeReload() {
|
||||
ac.mu.Lock()
|
||||
defer ac.mu.Unlock()
|
||||
|
||||
if ac.watcher.running {
|
||||
return // A watcher is running and will keep the cache up-to-date.
|
||||
}
|
||||
@@ -188,18 +193,22 @@ func (ac *addrCache) maybeReload() {
|
||||
ac.throttle.Reset(minReloadInterval)
|
||||
}
|
||||
|
||||
func (ac *addrCache) close() {
|
||||
func (ac *accountCache) close() {
|
||||
ac.mu.Lock()
|
||||
ac.watcher.close()
|
||||
if ac.throttle != nil {
|
||||
ac.throttle.Stop()
|
||||
}
|
||||
if ac.notify != nil {
|
||||
close(ac.notify)
|
||||
ac.notify = nil
|
||||
}
|
||||
ac.mu.Unlock()
|
||||
}
|
||||
|
||||
// reload caches addresses of existing accounts.
|
||||
// Callers must hold ac.mu.
|
||||
func (ac *addrCache) reload() {
|
||||
func (ac *accountCache) reload() {
|
||||
accounts, err := ac.scan()
|
||||
if err != nil && glog.V(logger.Debug) {
|
||||
glog.Errorf("can't load keys: %v", err)
|
||||
@@ -212,10 +221,14 @@ func (ac *addrCache) reload() {
|
||||
for _, a := range accounts {
|
||||
ac.byAddr[a.Address] = append(ac.byAddr[a.Address], a)
|
||||
}
|
||||
select {
|
||||
case ac.notify <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
glog.V(logger.Debug).Infof("reloaded keys, cache has %d accounts", len(ac.all))
|
||||
}
|
||||
|
||||
func (ac *addrCache) scan() ([]Account, error) {
|
||||
func (ac *accountCache) scan() ([]accounts.Account, error) {
|
||||
files, err := ioutil.ReadDir(ac.keydir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -223,7 +236,7 @@ func (ac *addrCache) scan() ([]Account, error) {
|
||||
|
||||
var (
|
||||
buf = new(bufio.Reader)
|
||||
addrs []Account
|
||||
addrs []accounts.Account
|
||||
keyJSON struct {
|
||||
Address string `json:"address"`
|
||||
}
|
||||
@@ -250,7 +263,7 @@ func (ac *addrCache) scan() ([]Account, error) {
|
||||
case (addr == common.Address{}):
|
||||
glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path)
|
||||
default:
|
||||
addrs = append(addrs, Account{Address: addr, File: path})
|
||||
addrs = append(addrs, accounts.Account{Address: addr, URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}})
|
||||
}
|
||||
fd.Close()
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -28,23 +28,24 @@ import (
|
||||
|
||||
"github.com/cespare/cp"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
var (
|
||||
cachetestDir, _ = filepath.Abs(filepath.Join("testdata", "keystore"))
|
||||
cachetestAccounts = []Account{
|
||||
cachetestAccounts = []accounts.Account{
|
||||
{
|
||||
Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
|
||||
File: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8")},
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
|
||||
File: filepath.Join(cachetestDir, "aaa"),
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")},
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"),
|
||||
File: filepath.Join(cachetestDir, "zzz"),
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")},
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -52,29 +53,36 @@ var (
|
||||
func TestWatchNewFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir, am := tmpManager(t, false)
|
||||
dir, ks := tmpKeyStore(t, false)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// Ensure the watcher is started before adding any files.
|
||||
am.Accounts()
|
||||
ks.Accounts()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Move in the files.
|
||||
wantAccounts := make([]Account, len(cachetestAccounts))
|
||||
wantAccounts := make([]accounts.Account, len(cachetestAccounts))
|
||||
for i := range cachetestAccounts {
|
||||
a := cachetestAccounts[i]
|
||||
a.File = filepath.Join(dir, filepath.Base(a.File))
|
||||
wantAccounts[i] = a
|
||||
if err := cp.CopyFile(a.File, cachetestAccounts[i].File); err != nil {
|
||||
wantAccounts[i] = accounts.Account{
|
||||
Address: cachetestAccounts[i].Address,
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))},
|
||||
}
|
||||
if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// am should see the accounts.
|
||||
var list []Account
|
||||
// ks should see the accounts.
|
||||
var list []accounts.Account
|
||||
for d := 200 * time.Millisecond; d < 5*time.Second; d *= 2 {
|
||||
list = am.Accounts()
|
||||
list = ks.Accounts()
|
||||
if reflect.DeepEqual(list, wantAccounts) {
|
||||
// ks should have also received change notifications
|
||||
select {
|
||||
case <-ks.changes:
|
||||
default:
|
||||
t.Fatalf("wasn't notified of new accounts")
|
||||
}
|
||||
return
|
||||
}
|
||||
time.Sleep(d)
|
||||
@@ -85,12 +93,12 @@ func TestWatchNewFile(t *testing.T) {
|
||||
func TestWatchNoDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create am but not the directory that it watches.
|
||||
// Create ks but not the directory that it watches.
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watch-test-%d-%d", os.Getpid(), rand.Int()))
|
||||
am := NewManager(dir, LightScryptN, LightScryptP)
|
||||
ks := NewKeyStore(dir, LightScryptN, LightScryptP)
|
||||
|
||||
list := am.Accounts()
|
||||
list := ks.Accounts()
|
||||
if len(list) > 0 {
|
||||
t.Error("initial account list not empty:", list)
|
||||
}
|
||||
@@ -100,16 +108,22 @@ func TestWatchNoDir(t *testing.T) {
|
||||
os.MkdirAll(dir, 0700)
|
||||
defer os.RemoveAll(dir)
|
||||
file := filepath.Join(dir, "aaa")
|
||||
if err := cp.CopyFile(file, cachetestAccounts[0].File); err != nil {
|
||||
if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// am should see the account.
|
||||
wantAccounts := []Account{cachetestAccounts[0]}
|
||||
wantAccounts[0].File = file
|
||||
// ks should see the account.
|
||||
wantAccounts := []accounts.Account{cachetestAccounts[0]}
|
||||
wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file}
|
||||
for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 {
|
||||
list = am.Accounts()
|
||||
list = ks.Accounts()
|
||||
if reflect.DeepEqual(list, wantAccounts) {
|
||||
// ks should have also received change notifications
|
||||
select {
|
||||
case <-ks.changes:
|
||||
default:
|
||||
t.Fatalf("wasn't notified of new accounts")
|
||||
}
|
||||
return
|
||||
}
|
||||
time.Sleep(d)
|
||||
@@ -118,7 +132,7 @@ func TestWatchNoDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCacheInitialReload(t *testing.T) {
|
||||
cache := newAddrCache(cachetestDir)
|
||||
cache, _ := newAccountCache(cachetestDir)
|
||||
accounts := cache.accounts()
|
||||
if !reflect.DeepEqual(accounts, cachetestAccounts) {
|
||||
t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts))
|
||||
@@ -126,55 +140,55 @@ func TestCacheInitialReload(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCacheAddDeleteOrder(t *testing.T) {
|
||||
cache := newAddrCache("testdata/no-such-dir")
|
||||
cache, _ := newAccountCache("testdata/no-such-dir")
|
||||
cache.watcher.running = true // prevent unexpected reloads
|
||||
|
||||
accounts := []Account{
|
||||
accs := []accounts.Account{
|
||||
{
|
||||
Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"),
|
||||
File: "-309830980",
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"},
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"),
|
||||
File: "ggg",
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"},
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"),
|
||||
File: "zzzzzz-the-very-last-one.keyXXX",
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"},
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
|
||||
File: "SOMETHING.key",
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"},
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
|
||||
File: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8",
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"},
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
|
||||
File: "aaa",
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"},
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"),
|
||||
File: "zzz",
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"},
|
||||
},
|
||||
}
|
||||
for _, a := range accounts {
|
||||
for _, a := range accs {
|
||||
cache.add(a)
|
||||
}
|
||||
// Add some of them twice to check that they don't get reinserted.
|
||||
cache.add(accounts[0])
|
||||
cache.add(accounts[2])
|
||||
cache.add(accs[0])
|
||||
cache.add(accs[2])
|
||||
|
||||
// Check that the account list is sorted by filename.
|
||||
wantAccounts := make([]Account, len(accounts))
|
||||
copy(wantAccounts, accounts)
|
||||
sort.Sort(accountsByFile(wantAccounts))
|
||||
wantAccounts := make([]accounts.Account, len(accs))
|
||||
copy(wantAccounts, accs)
|
||||
sort.Sort(accountsByURL(wantAccounts))
|
||||
list := cache.accounts()
|
||||
if !reflect.DeepEqual(list, wantAccounts) {
|
||||
t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accounts), spew.Sdump(wantAccounts))
|
||||
t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts))
|
||||
}
|
||||
for _, a := range accounts {
|
||||
for _, a := range accs {
|
||||
if !cache.hasAddress(a.Address) {
|
||||
t.Errorf("expected hasAccount(%x) to return true", a.Address)
|
||||
}
|
||||
@@ -184,13 +198,13 @@ func TestCacheAddDeleteOrder(t *testing.T) {
|
||||
}
|
||||
|
||||
// Delete a few keys from the cache.
|
||||
for i := 0; i < len(accounts); i += 2 {
|
||||
for i := 0; i < len(accs); i += 2 {
|
||||
cache.delete(wantAccounts[i])
|
||||
}
|
||||
cache.delete(Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), File: "something"})
|
||||
cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}})
|
||||
|
||||
// Check content again after deletion.
|
||||
wantAccountsAfterDelete := []Account{
|
||||
wantAccountsAfterDelete := []accounts.Account{
|
||||
wantAccounts[1],
|
||||
wantAccounts[3],
|
||||
wantAccounts[5],
|
||||
@@ -211,63 +225,63 @@ func TestCacheAddDeleteOrder(t *testing.T) {
|
||||
|
||||
func TestCacheFind(t *testing.T) {
|
||||
dir := filepath.Join("testdata", "dir")
|
||||
cache := newAddrCache(dir)
|
||||
cache, _ := newAccountCache(dir)
|
||||
cache.watcher.running = true // prevent unexpected reloads
|
||||
|
||||
accounts := []Account{
|
||||
accs := []accounts.Account{
|
||||
{
|
||||
Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"),
|
||||
File: filepath.Join(dir, "a.key"),
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")},
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"),
|
||||
File: filepath.Join(dir, "b.key"),
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")},
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
|
||||
File: filepath.Join(dir, "c.key"),
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")},
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
|
||||
File: filepath.Join(dir, "c2.key"),
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")},
|
||||
},
|
||||
}
|
||||
for _, a := range accounts {
|
||||
for _, a := range accs {
|
||||
cache.add(a)
|
||||
}
|
||||
|
||||
nomatchAccount := Account{
|
||||
nomatchAccount := accounts.Account{
|
||||
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
|
||||
File: filepath.Join(dir, "something"),
|
||||
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")},
|
||||
}
|
||||
tests := []struct {
|
||||
Query Account
|
||||
WantResult Account
|
||||
Query accounts.Account
|
||||
WantResult accounts.Account
|
||||
WantError error
|
||||
}{
|
||||
// by address
|
||||
{Query: Account{Address: accounts[0].Address}, WantResult: accounts[0]},
|
||||
{Query: accounts.Account{Address: accs[0].Address}, WantResult: accs[0]},
|
||||
// by file
|
||||
{Query: Account{File: accounts[0].File}, WantResult: accounts[0]},
|
||||
{Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]},
|
||||
// by basename
|
||||
{Query: Account{File: filepath.Base(accounts[0].File)}, WantResult: accounts[0]},
|
||||
{Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, WantResult: accs[0]},
|
||||
// by file and address
|
||||
{Query: accounts[0], WantResult: accounts[0]},
|
||||
{Query: accs[0], WantResult: accs[0]},
|
||||
// ambiguous address, tie resolved by file
|
||||
{Query: accounts[2], WantResult: accounts[2]},
|
||||
{Query: accs[2], WantResult: accs[2]},
|
||||
// ambiguous address error
|
||||
{
|
||||
Query: Account{Address: accounts[2].Address},
|
||||
Query: accounts.Account{Address: accs[2].Address},
|
||||
WantError: &AmbiguousAddrError{
|
||||
Addr: accounts[2].Address,
|
||||
Matches: []Account{accounts[2], accounts[3]},
|
||||
Addr: accs[2].Address,
|
||||
Matches: []accounts.Account{accs[2], accs[3]},
|
||||
},
|
||||
},
|
||||
// no match error
|
||||
{Query: nomatchAccount, WantError: ErrNoMatch},
|
||||
{Query: Account{File: nomatchAccount.File}, WantError: ErrNoMatch},
|
||||
{Query: Account{File: filepath.Base(nomatchAccount.File)}, WantError: ErrNoMatch},
|
||||
{Query: Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch},
|
||||
{Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch},
|
||||
{Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(nomatchAccount.URL.Path)}}, WantError: ErrNoMatch},
|
||||
{Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch},
|
||||
}
|
||||
for i, test := range tests {
|
||||
a, err := cache.find(test.Query)
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/crypto/secp256k1"
|
||||
@@ -175,13 +176,13 @@ func newKey(rand io.Reader) (*Key, error) {
|
||||
return newKeyFromECDSA(privateKeyECDSA), nil
|
||||
}
|
||||
|
||||
func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, Account, error) {
|
||||
func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Account, error) {
|
||||
key, err := newKey(rand)
|
||||
if err != nil {
|
||||
return nil, Account{}, err
|
||||
return nil, accounts.Account{}, err
|
||||
}
|
||||
a := Account{Address: key.Address, File: ks.JoinPath(keyFileName(key.Address))}
|
||||
if err := ks.StoreKey(a.File, key, auth); err != nil {
|
||||
a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}}
|
||||
if err := ks.StoreKey(a.URL.Path, key, auth); err != nil {
|
||||
zeroKey(key.PrivateKey)
|
||||
return nil, a, err
|
||||
}
|
||||
494
accounts/keystore/keystore.go
Normal file
494
accounts/keystore/keystore.go
Normal file
@@ -0,0 +1,494 @@
|
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package keystore implements encrypted storage of secp256k1 private keys.
|
||||
//
|
||||
// Keys are stored as encrypted JSON files according to the Web3 Secret Storage specification.
|
||||
// See https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition for more information.
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
crand "crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrLocked = accounts.NewAuthNeededError("password or unlock")
|
||||
ErrNoMatch = errors.New("no key for given address or file")
|
||||
ErrDecrypt = errors.New("could not decrypt key with given passphrase")
|
||||
)
|
||||
|
||||
// KeyStoreType is the reflect type of a keystore backend.
|
||||
var KeyStoreType = reflect.TypeOf(&KeyStore{})
|
||||
|
||||
// KeyStoreScheme is the protocol scheme prefixing account and wallet URLs.
|
||||
var KeyStoreScheme = "keystore"
|
||||
|
||||
// Maximum time between wallet refreshes (if filesystem notifications don't work).
|
||||
const walletRefreshCycle = 3 * time.Second
|
||||
|
||||
// KeyStore manages a key storage directory on disk.
|
||||
type KeyStore struct {
|
||||
storage keyStore // Storage backend, might be cleartext or encrypted
|
||||
cache *accountCache // In-memory account cache over the filesystem storage
|
||||
changes chan struct{} // Channel receiving change notifications from the cache
|
||||
unlocked map[common.Address]*unlocked // Currently unlocked account (decrypted private keys)
|
||||
|
||||
wallets []accounts.Wallet // Wallet wrappers around the individual key files
|
||||
updateFeed event.Feed // Event feed to notify wallet additions/removals
|
||||
updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
|
||||
updating bool // Whether the event notification loop is running
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type unlocked struct {
|
||||
*Key
|
||||
abort chan struct{}
|
||||
}
|
||||
|
||||
// NewKeyStore creates a keystore for the given directory.
|
||||
func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore {
|
||||
keydir, _ = filepath.Abs(keydir)
|
||||
ks := &KeyStore{storage: &keyStorePassphrase{keydir, scryptN, scryptP}}
|
||||
ks.init(keydir)
|
||||
return ks
|
||||
}
|
||||
|
||||
// NewPlaintextKeyStore creates a keystore for the given directory.
|
||||
// Deprecated: Use NewKeyStore.
|
||||
func NewPlaintextKeyStore(keydir string) *KeyStore {
|
||||
keydir, _ = filepath.Abs(keydir)
|
||||
ks := &KeyStore{storage: &keyStorePlain{keydir}}
|
||||
ks.init(keydir)
|
||||
return ks
|
||||
}
|
||||
|
||||
func (ks *KeyStore) init(keydir string) {
|
||||
// Lock the mutex since the account cache might call back with events
|
||||
ks.mu.Lock()
|
||||
defer ks.mu.Unlock()
|
||||
|
||||
// Initialize the set of unlocked keys and the account cache
|
||||
ks.unlocked = make(map[common.Address]*unlocked)
|
||||
ks.cache, ks.changes = newAccountCache(keydir)
|
||||
|
||||
// TODO: In order for this finalizer to work, there must be no references
|
||||
// to ks. addressCache doesn't keep a reference but unlocked keys do,
|
||||
// so the finalizer will not trigger until all timed unlocks have expired.
|
||||
runtime.SetFinalizer(ks, func(m *KeyStore) {
|
||||
m.cache.close()
|
||||
})
|
||||
// Create the initial list of wallets from the cache
|
||||
accs := ks.cache.accounts()
|
||||
ks.wallets = make([]accounts.Wallet, len(accs))
|
||||
for i := 0; i < len(accs); i++ {
|
||||
ks.wallets[i] = &keystoreWallet{account: accs[i], keystore: ks}
|
||||
}
|
||||
}
|
||||
|
||||
// Wallets implements accounts.Backend, returning all single-key wallets from the
|
||||
// keystore directory.
|
||||
func (ks *KeyStore) Wallets() []accounts.Wallet {
|
||||
// Make sure the list of wallets is in sync with the account cache
|
||||
ks.refreshWallets()
|
||||
|
||||
ks.mu.RLock()
|
||||
defer ks.mu.RUnlock()
|
||||
|
||||
cpy := make([]accounts.Wallet, len(ks.wallets))
|
||||
copy(cpy, ks.wallets)
|
||||
return cpy
|
||||
}
|
||||
|
||||
// refreshWallets retrieves the current account list and based on that does any
|
||||
// necessary wallet refreshes.
|
||||
func (ks *KeyStore) refreshWallets() {
|
||||
// Retrieve the current list of accounts
|
||||
ks.mu.Lock()
|
||||
accs := ks.cache.accounts()
|
||||
|
||||
// Transform the current list of wallets into the new one
|
||||
wallets := make([]accounts.Wallet, 0, len(accs))
|
||||
events := []accounts.WalletEvent{}
|
||||
|
||||
for _, account := range accs {
|
||||
// Drop wallets while they were in front of the next account
|
||||
for len(ks.wallets) > 0 && ks.wallets[0].URL().Cmp(account.URL) < 0 {
|
||||
events = append(events, accounts.WalletEvent{Wallet: ks.wallets[0], Arrive: false})
|
||||
ks.wallets = ks.wallets[1:]
|
||||
}
|
||||
// If there are no more wallets or the account is before the next, wrap new wallet
|
||||
if len(ks.wallets) == 0 || ks.wallets[0].URL().Cmp(account.URL) > 0 {
|
||||
wallet := &keystoreWallet{account: account, keystore: ks}
|
||||
|
||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true})
|
||||
wallets = append(wallets, wallet)
|
||||
continue
|
||||
}
|
||||
// If the account is the same as the first wallet, keep it
|
||||
if ks.wallets[0].Accounts()[0] == account {
|
||||
wallets = append(wallets, ks.wallets[0])
|
||||
ks.wallets = ks.wallets[1:]
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Drop any leftover wallets and set the new batch
|
||||
for _, wallet := range ks.wallets {
|
||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false})
|
||||
}
|
||||
ks.wallets = wallets
|
||||
ks.mu.Unlock()
|
||||
|
||||
// Fire all wallet events and return
|
||||
for _, event := range events {
|
||||
ks.updateFeed.Send(event)
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe implements accounts.Backend, creating an async subscription to
|
||||
// receive notifications on the addition or removal of keystore wallets.
|
||||
func (ks *KeyStore) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
|
||||
// We need the mutex to reliably start/stop the update loop
|
||||
ks.mu.Lock()
|
||||
defer ks.mu.Unlock()
|
||||
|
||||
// Subscribe the caller and track the subscriber count
|
||||
sub := ks.updateScope.Track(ks.updateFeed.Subscribe(sink))
|
||||
|
||||
// Subscribers require an active notification loop, start it
|
||||
if !ks.updating {
|
||||
ks.updating = true
|
||||
go ks.updater()
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
// updater is responsible for maintaining an up-to-date list of wallets stored in
|
||||
// the keystore, and for firing wallet addition/removal events. It listens for
|
||||
// account change events from the underlying account cache, and also periodically
|
||||
// forces a manual refresh (only triggers for systems where the filesystem notifier
|
||||
// is not running).
|
||||
func (ks *KeyStore) updater() {
|
||||
for {
|
||||
// Wait for an account update or a refresh timeout
|
||||
select {
|
||||
case <-ks.changes:
|
||||
case <-time.After(walletRefreshCycle):
|
||||
}
|
||||
// Run the wallet refresher
|
||||
ks.refreshWallets()
|
||||
|
||||
// If all our subscribers left, stop the updater
|
||||
ks.mu.Lock()
|
||||
if ks.updateScope.Count() == 0 {
|
||||
ks.updating = false
|
||||
ks.mu.Unlock()
|
||||
return
|
||||
}
|
||||
ks.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// HasAddress reports whether a key with the given address is present.
|
||||
func (ks *KeyStore) HasAddress(addr common.Address) bool {
|
||||
return ks.cache.hasAddress(addr)
|
||||
}
|
||||
|
||||
// Accounts returns all key files present in the directory.
|
||||
func (ks *KeyStore) Accounts() []accounts.Account {
|
||||
return ks.cache.accounts()
|
||||
}
|
||||
|
||||
// Delete deletes the key matched by account if the passphrase is correct.
|
||||
// If the account contains no filename, the address must match a unique key.
|
||||
func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error {
|
||||
// Decrypting the key isn't really necessary, but we do
|
||||
// it anyway to check the password and zero out the key
|
||||
// immediately afterwards.
|
||||
a, key, err := ks.getDecryptedKey(a, passphrase)
|
||||
if key != nil {
|
||||
zeroKey(key.PrivateKey)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The order is crucial here. The key is dropped from the
|
||||
// cache after the file is gone so that a reload happening in
|
||||
// between won't insert it into the cache again.
|
||||
err = os.Remove(a.URL.Path)
|
||||
if err == nil {
|
||||
ks.cache.delete(a)
|
||||
ks.refreshWallets()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// SignHash calculates a ECDSA signature for the given hash. The produced
|
||||
// signature is in the [R || S || V] format where V is 0 or 1.
|
||||
func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) {
|
||||
// Look up the key to sign with and abort if it cannot be found
|
||||
ks.mu.RLock()
|
||||
defer ks.mu.RUnlock()
|
||||
|
||||
unlockedKey, found := ks.unlocked[a.Address]
|
||||
if !found {
|
||||
return nil, ErrLocked
|
||||
}
|
||||
// Sign the hash using plain ECDSA operations
|
||||
return crypto.Sign(hash, unlockedKey.PrivateKey)
|
||||
}
|
||||
|
||||
// SignTx signs the given transaction with the requested account.
|
||||
func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||
// Look up the key to sign with and abort if it cannot be found
|
||||
ks.mu.RLock()
|
||||
defer ks.mu.RUnlock()
|
||||
|
||||
unlockedKey, found := ks.unlocked[a.Address]
|
||||
if !found {
|
||||
return nil, ErrLocked
|
||||
}
|
||||
// Depending on the presence of the chain ID, sign with EIP155 or homestead
|
||||
if chainID != nil {
|
||||
return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey)
|
||||
}
|
||||
return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey)
|
||||
}
|
||||
|
||||
// SignHashWithPassphrase signs hash if the private key matching the given address
|
||||
// can be decrypted with the given passphrase. The produced signature is in the
|
||||
// [R || S || V] format where V is 0 or 1.
|
||||
func (ks *KeyStore) SignHashWithPassphrase(a accounts.Account, passphrase string, hash []byte) (signature []byte, err error) {
|
||||
_, key, err := ks.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer zeroKey(key.PrivateKey)
|
||||
return crypto.Sign(hash, key.PrivateKey)
|
||||
}
|
||||
|
||||
// SignTxWithPassphrase signs the transaction if the private key matching the
|
||||
// given address can be decrypted with the given passphrase.
|
||||
func (ks *KeyStore) SignTxWithPassphrase(a accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||
_, key, err := ks.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer zeroKey(key.PrivateKey)
|
||||
|
||||
// Depending on the presence of the chain ID, sign with EIP155 or homestead
|
||||
if chainID != nil {
|
||||
return types.SignTx(tx, types.NewEIP155Signer(chainID), key.PrivateKey)
|
||||
}
|
||||
return types.SignTx(tx, types.HomesteadSigner{}, key.PrivateKey)
|
||||
}
|
||||
|
||||
// Unlock unlocks the given account indefinitely.
|
||||
func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error {
|
||||
return ks.TimedUnlock(a, passphrase, 0)
|
||||
}
|
||||
|
||||
// Lock removes the private key with the given address from memory.
|
||||
func (ks *KeyStore) Lock(addr common.Address) error {
|
||||
ks.mu.Lock()
|
||||
if unl, found := ks.unlocked[addr]; found {
|
||||
ks.mu.Unlock()
|
||||
ks.expire(addr, unl, time.Duration(0)*time.Nanosecond)
|
||||
} else {
|
||||
ks.mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TimedUnlock unlocks the given account with the passphrase. The account
|
||||
// stays unlocked for the duration of timeout. A timeout of 0 unlocks the account
|
||||
// until the program exits. The account must match a unique key file.
|
||||
//
|
||||
// If the account address is already unlocked for a duration, TimedUnlock extends or
|
||||
// shortens the active unlock timeout. If the address was previously unlocked
|
||||
// indefinitely the timeout is not altered.
|
||||
func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error {
|
||||
a, key, err := ks.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ks.mu.Lock()
|
||||
defer ks.mu.Unlock()
|
||||
u, found := ks.unlocked[a.Address]
|
||||
if found {
|
||||
if u.abort == nil {
|
||||
// The address was unlocked indefinitely, so unlocking
|
||||
// it with a timeout would be confusing.
|
||||
zeroKey(key.PrivateKey)
|
||||
return nil
|
||||
}
|
||||
// Terminate the expire goroutine and replace it below.
|
||||
close(u.abort)
|
||||
}
|
||||
if timeout > 0 {
|
||||
u = &unlocked{Key: key, abort: make(chan struct{})}
|
||||
go ks.expire(a.Address, u, timeout)
|
||||
} else {
|
||||
u = &unlocked{Key: key}
|
||||
}
|
||||
ks.unlocked[a.Address] = u
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find resolves the given account into a unique entry in the keystore.
|
||||
func (ks *KeyStore) Find(a accounts.Account) (accounts.Account, error) {
|
||||
ks.cache.maybeReload()
|
||||
ks.cache.mu.Lock()
|
||||
a, err := ks.cache.find(a)
|
||||
ks.cache.mu.Unlock()
|
||||
return a, err
|
||||
}
|
||||
|
||||
func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) {
|
||||
a, err := ks.Find(a)
|
||||
if err != nil {
|
||||
return a, nil, err
|
||||
}
|
||||
key, err := ks.storage.GetKey(a.Address, a.URL.Path, auth)
|
||||
return a, key, err
|
||||
}
|
||||
|
||||
func (ks *KeyStore) expire(addr common.Address, u *unlocked, timeout time.Duration) {
|
||||
t := time.NewTimer(timeout)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case <-u.abort:
|
||||
// just quit
|
||||
case <-t.C:
|
||||
ks.mu.Lock()
|
||||
// only drop if it's still the same key instance that dropLater
|
||||
// was launched with. we can check that using pointer equality
|
||||
// because the map stores a new pointer every time the key is
|
||||
// unlocked.
|
||||
if ks.unlocked[addr] == u {
|
||||
zeroKey(u.PrivateKey)
|
||||
delete(ks.unlocked, addr)
|
||||
}
|
||||
ks.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// NewAccount generates a new key and stores it into the key directory,
|
||||
// encrypting it with the passphrase.
|
||||
func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) {
|
||||
_, account, err := storeNewKey(ks.storage, crand.Reader, passphrase)
|
||||
if err != nil {
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
// Add the account to the cache immediately rather
|
||||
// than waiting for file system notifications to pick it up.
|
||||
ks.cache.add(account)
|
||||
ks.refreshWallets()
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// Export exports as a JSON key, encrypted with newPassphrase.
|
||||
func (ks *KeyStore) Export(a accounts.Account, passphrase, newPassphrase string) (keyJSON []byte, err error) {
|
||||
_, key, err := ks.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var N, P int
|
||||
if store, ok := ks.storage.(*keyStorePassphrase); ok {
|
||||
N, P = store.scryptN, store.scryptP
|
||||
} else {
|
||||
N, P = StandardScryptN, StandardScryptP
|
||||
}
|
||||
return EncryptKey(key, newPassphrase, N, P)
|
||||
}
|
||||
|
||||
// Import stores the given encrypted JSON key into the key directory.
|
||||
func (ks *KeyStore) Import(keyJSON []byte, passphrase, newPassphrase string) (accounts.Account, error) {
|
||||
key, err := DecryptKey(keyJSON, passphrase)
|
||||
if key != nil && key.PrivateKey != nil {
|
||||
defer zeroKey(key.PrivateKey)
|
||||
}
|
||||
if err != nil {
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
return ks.importKey(key, newPassphrase)
|
||||
}
|
||||
|
||||
// ImportECDSA stores the given key into the key directory, encrypting it with the passphrase.
|
||||
func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (accounts.Account, error) {
|
||||
key := newKeyFromECDSA(priv)
|
||||
if ks.cache.hasAddress(key.Address) {
|
||||
return accounts.Account{}, fmt.Errorf("account already exists")
|
||||
}
|
||||
|
||||
return ks.importKey(key, passphrase)
|
||||
}
|
||||
|
||||
func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) {
|
||||
a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}}
|
||||
if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil {
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
ks.cache.add(a)
|
||||
ks.refreshWallets()
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Update changes the passphrase of an existing account.
|
||||
func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error {
|
||||
a, key, err := ks.getDecryptedKey(a, passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ks.storage.StoreKey(a.URL.Path, key, newPassphrase)
|
||||
}
|
||||
|
||||
// ImportPreSaleKey decrypts the given Ethereum presale wallet and stores
|
||||
// a key file in the key directory. The key file is encrypted with the same passphrase.
|
||||
func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) {
|
||||
a, _, err := importPreSaleKey(ks.storage, keyJSON, passphrase)
|
||||
if err != nil {
|
||||
return a, err
|
||||
}
|
||||
ks.cache.add(a)
|
||||
ks.refreshWallets()
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// zeroKey zeroes a private key in memory.
|
||||
func zeroKey(k *ecdsa.PrivateKey) {
|
||||
b := k.D.Bits()
|
||||
for i := range b {
|
||||
b[i] = 0
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ The crypto is documented at https://github.com/ethereum/wiki/wiki/Web3-Secret-St
|
||||
|
||||
*/
|
||||
|
||||
package accounts
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
func tmpKeyStore(t *testing.T, encrypted bool) (dir string, ks keyStore) {
|
||||
func tmpKeyStoreIface(t *testing.T, encrypted bool) (dir string, ks keyStore) {
|
||||
d, err := ioutil.TempDir("", "geth-keystore-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -44,7 +44,7 @@ func tmpKeyStore(t *testing.T, encrypted bool) (dir string, ks keyStore) {
|
||||
}
|
||||
|
||||
func TestKeyStorePlain(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, false)
|
||||
dir, ks := tmpKeyStoreIface(t, false)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "" // not used but required by API
|
||||
@@ -52,7 +52,7 @@ func TestKeyStorePlain(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
k2, err := ks.GetKey(k1.Address, account.File, pass)
|
||||
k2, err := ks.GetKey(k1.Address, account.URL.Path, pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func TestKeyStorePlain(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestKeyStorePassphrase(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, true)
|
||||
dir, ks := tmpKeyStoreIface(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "foo"
|
||||
@@ -73,7 +73,7 @@ func TestKeyStorePassphrase(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
k2, err := ks.GetKey(k1.Address, account.File, pass)
|
||||
k2, err := ks.GetKey(k1.Address, account.URL.Path, pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func TestKeyStorePassphrase(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestKeyStorePassphraseDecryptionFail(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, true)
|
||||
dir, ks := tmpKeyStoreIface(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "foo"
|
||||
@@ -94,13 +94,13 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = ks.GetKey(k1.Address, account.File, "bar"); err != ErrDecrypt {
|
||||
if _, err = ks.GetKey(k1.Address, account.URL.Path, "bar"); err != ErrDecrypt {
|
||||
t.Fatalf("wrong error for invalid passphrase\ngot %q\nwant %q", err, ErrDecrypt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportPreSaleKey(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, true)
|
||||
dir, ks := tmpKeyStoreIface(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// file content of a presale key file generated with:
|
||||
@@ -115,8 +115,8 @@ func TestImportPreSaleKey(t *testing.T) {
|
||||
if account.Address != common.HexToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") {
|
||||
t.Errorf("imported account has wrong address %x", account.Address)
|
||||
}
|
||||
if !strings.HasPrefix(account.File, dir) {
|
||||
t.Errorf("imported account file not in keystore directory: %q", account.File)
|
||||
if !strings.HasPrefix(account.URL.Path, dir) {
|
||||
t.Errorf("imported account file not in keystore directory: %q", account.URL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,19 +142,19 @@ func TestV3_PBKDF2_1(t *testing.T) {
|
||||
|
||||
func TestV3_PBKDF2_2(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t)
|
||||
tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
|
||||
testDecryptV3(tests["test1"], t)
|
||||
}
|
||||
|
||||
func TestV3_PBKDF2_3(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t)
|
||||
tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
|
||||
testDecryptV3(tests["python_generated_test_with_odd_iv"], t)
|
||||
}
|
||||
|
||||
func TestV3_PBKDF2_4(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t)
|
||||
tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
|
||||
testDecryptV3(tests["evilnonce"], t)
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ func TestV3_Scrypt_1(t *testing.T) {
|
||||
|
||||
func TestV3_Scrypt_2(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t)
|
||||
tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
|
||||
testDecryptV3(tests["test2"], t)
|
||||
}
|
||||
|
||||
365
accounts/keystore/keystore_test.go
Normal file
365
accounts/keystore/keystore_test.go
Normal file
@@ -0,0 +1,365 @@
|
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
)
|
||||
|
||||
var testSigData = make([]byte, 32)
|
||||
|
||||
func TestKeyStore(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
a, err := ks.NewAccount("foo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(a.URL.Path, dir) {
|
||||
t.Errorf("account file %s doesn't have dir prefix", a.URL)
|
||||
}
|
||||
stat, err := os.Stat(a.URL.Path)
|
||||
if err != nil {
|
||||
t.Fatalf("account file %s doesn't exist (%v)", a.URL, err)
|
||||
}
|
||||
if runtime.GOOS != "windows" && stat.Mode() != 0600 {
|
||||
t.Fatalf("account file has wrong mode: got %o, want %o", stat.Mode(), 0600)
|
||||
}
|
||||
if !ks.HasAddress(a.Address) {
|
||||
t.Errorf("HasAccount(%x) should've returned true", a.Address)
|
||||
}
|
||||
if err := ks.Update(a, "foo", "bar"); err != nil {
|
||||
t.Errorf("Update error: %v", err)
|
||||
}
|
||||
if err := ks.Delete(a, "bar"); err != nil {
|
||||
t.Errorf("Delete error: %v", err)
|
||||
}
|
||||
if common.FileExist(a.URL.Path) {
|
||||
t.Errorf("account file %s should be gone after Delete", a.URL)
|
||||
}
|
||||
if ks.HasAddress(a.Address) {
|
||||
t.Errorf("HasAccount(%x) should've returned true after Delete", a.Address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "" // not used but required by API
|
||||
a1, err := ks.NewAccount(pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ks.Unlock(a1, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithPassphrase(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "passwd"
|
||||
acc, err := ks.NewAccount(pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, unlocked := ks.unlocked[acc.Address]; unlocked {
|
||||
t.Fatal("expected account to be locked")
|
||||
}
|
||||
|
||||
_, err = ks.SignHashWithPassphrase(acc, pass, testSigData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, unlocked := ks.unlocked[acc.Address]; unlocked {
|
||||
t.Fatal("expected account to be locked")
|
||||
}
|
||||
|
||||
if _, err = ks.SignHashWithPassphrase(acc, "invalid passwd", testSigData); err == nil {
|
||||
t.Fatal("expected SignHashWithPassphrase to fail with invalid password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimedUnlock(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, true)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "foo"
|
||||
a1, err := ks.NewAccount(pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Signing without passphrase fails because account is locked
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
||||
if err != ErrLocked {
|
||||
t.Fatal("Signing should've failed with ErrLocked before unlocking, got ", err)
|
||||
}
|
||||
|
||||
// Signing with passphrase works
|
||||
if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Signing without passphrase works because account is temp unlocked
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
||||
if err != nil {
|
||||
t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
|
||||
}
|
||||
|
||||
// Signing fails again after automatic locking
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
||||
if err != ErrLocked {
|
||||
t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverrideUnlock(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, false)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pass := "foo"
|
||||
a1, err := ks.NewAccount(pass)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Unlock indefinitely.
|
||||
if err = ks.TimedUnlock(a1, pass, 5*time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Signing without passphrase works because account is temp unlocked
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
||||
if err != nil {
|
||||
t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
|
||||
}
|
||||
|
||||
// reset unlock to a shorter period, invalidates the previous unlock
|
||||
if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Signing without passphrase still works because account is temp unlocked
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
||||
if err != nil {
|
||||
t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
|
||||
}
|
||||
|
||||
// Signing fails again after automatic locking
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
||||
if err != ErrLocked {
|
||||
t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// This test should fail under -race if signing races the expiration goroutine.
|
||||
func TestSignRace(t *testing.T) {
|
||||
dir, ks := tmpKeyStore(t, false)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// Create a test account.
|
||||
a1, err := ks.NewAccount("")
|
||||
if err != nil {
|
||||
t.Fatal("could not create the test account", err)
|
||||
}
|
||||
|
||||
if err := ks.TimedUnlock(a1, "", 15*time.Millisecond); err != nil {
|
||||
t.Fatal("could not unlock the test account", err)
|
||||
}
|
||||
end := time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(end) {
|
||||
if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err == ErrLocked {
|
||||
return
|
||||
} else if err != nil {
|
||||
t.Errorf("Sign error: %v", err)
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
t.Errorf("Account did not lock within the timeout")
|
||||
}
|
||||
|
||||
// Tests that the wallet notifier loop starts and stops correctly based on the
|
||||
// addition and removal of wallet event subscriptions.
|
||||
func TestWalletNotifierLifecycle(t *testing.T) {
|
||||
// Create a temporary kesytore to test with
|
||||
dir, ks := tmpKeyStore(t, false)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// Ensure that the notification updater is not running yet
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
ks.mu.RLock()
|
||||
updating := ks.updating
|
||||
ks.mu.RUnlock()
|
||||
|
||||
if updating {
|
||||
t.Errorf("wallet notifier running without subscribers")
|
||||
}
|
||||
// Subscribe to the wallet feed and ensure the updater boots up
|
||||
updates := make(chan accounts.WalletEvent)
|
||||
|
||||
subs := make([]event.Subscription, 2)
|
||||
for i := 0; i < len(subs); i++ {
|
||||
// Create a new subscription
|
||||
subs[i] = ks.Subscribe(updates)
|
||||
|
||||
// Ensure the notifier comes online
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
ks.mu.RLock()
|
||||
updating = ks.updating
|
||||
ks.mu.RUnlock()
|
||||
|
||||
if !updating {
|
||||
t.Errorf("sub %d: wallet notifier not running after subscription", i)
|
||||
}
|
||||
}
|
||||
// Unsubscribe and ensure the updater terminates eventually
|
||||
for i := 0; i < len(subs); i++ {
|
||||
// Close an existing subscription
|
||||
subs[i].Unsubscribe()
|
||||
|
||||
// Ensure the notifier shuts down at and only at the last close
|
||||
for k := 0; k < int(walletRefreshCycle/(250*time.Millisecond))+2; k++ {
|
||||
ks.mu.RLock()
|
||||
updating = ks.updating
|
||||
ks.mu.RUnlock()
|
||||
|
||||
if i < len(subs)-1 && !updating {
|
||||
t.Fatalf("sub %d: event notifier stopped prematurely", i)
|
||||
}
|
||||
if i == len(subs)-1 && !updating {
|
||||
return
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
t.Errorf("wallet notifier didn't terminate after unsubscribe")
|
||||
}
|
||||
|
||||
// Tests that wallet notifications and correctly fired when accounts are added
|
||||
// or deleted from the keystore.
|
||||
func TestWalletNotifications(t *testing.T) {
|
||||
// Create a temporary kesytore to test with
|
||||
dir, ks := tmpKeyStore(t, false)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// Subscribe to the wallet feed
|
||||
updates := make(chan accounts.WalletEvent, 1)
|
||||
sub := ks.Subscribe(updates)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// Randomly add and remove account and make sure events and wallets are in sync
|
||||
live := make(map[common.Address]accounts.Account)
|
||||
for i := 0; i < 1024; i++ {
|
||||
// Execute a creation or deletion and ensure event arrival
|
||||
if create := len(live) == 0 || rand.Int()%4 > 0; create {
|
||||
// Add a new account and ensure wallet notifications arrives
|
||||
account, err := ks.NewAccount("")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test account: %v", err)
|
||||
}
|
||||
select {
|
||||
case event := <-updates:
|
||||
if !event.Arrive {
|
||||
t.Errorf("departure event on account creation")
|
||||
}
|
||||
if event.Wallet.Accounts()[0] != account {
|
||||
t.Errorf("account mismatch on created wallet: have %v, want %v", event.Wallet.Accounts()[0], account)
|
||||
}
|
||||
default:
|
||||
t.Errorf("wallet arrival event not fired on account creation")
|
||||
}
|
||||
live[account.Address] = account
|
||||
} else {
|
||||
// Select a random account to delete (crude, but works)
|
||||
var account accounts.Account
|
||||
for _, a := range live {
|
||||
account = a
|
||||
break
|
||||
}
|
||||
// Remove an account and ensure wallet notifiaction arrives
|
||||
if err := ks.Delete(account, ""); err != nil {
|
||||
t.Fatalf("failed to delete test account: %v", err)
|
||||
}
|
||||
select {
|
||||
case event := <-updates:
|
||||
if event.Arrive {
|
||||
t.Errorf("arrival event on account deletion")
|
||||
}
|
||||
if event.Wallet.Accounts()[0] != account {
|
||||
t.Errorf("account mismatch on deleted wallet: have %v, want %v", event.Wallet.Accounts()[0], account)
|
||||
}
|
||||
default:
|
||||
t.Errorf("wallet departure event not fired on account creation")
|
||||
}
|
||||
delete(live, account.Address)
|
||||
}
|
||||
// Retrieve the list of wallets and ensure it matches with our required live set
|
||||
liveList := make([]accounts.Account, 0, len(live))
|
||||
for _, account := range live {
|
||||
liveList = append(liveList, account)
|
||||
}
|
||||
sort.Sort(accountsByURL(liveList))
|
||||
|
||||
wallets := ks.Wallets()
|
||||
if len(liveList) != len(wallets) {
|
||||
t.Errorf("wallet list doesn't match required accounts: have %v, want %v", wallets, liveList)
|
||||
} else {
|
||||
for j, wallet := range wallets {
|
||||
if accs := wallet.Accounts(); len(accs) != 1 {
|
||||
t.Errorf("wallet %d: contains invalid number of accounts: have %d, want 1", j, len(accs))
|
||||
} else if accs[0] != liveList[j] {
|
||||
t.Errorf("wallet %d: account mismatch: have %v, want %v", j, accs[0], liveList[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tmpKeyStore(t *testing.T, encrypted bool) (string, *KeyStore) {
|
||||
d, err := ioutil.TempDir("", "eth-keystore-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
new := NewPlaintextKeyStore
|
||||
if encrypted {
|
||||
new = func(kd string) *KeyStore { return NewKeyStore(kd, veryLightScryptN, veryLightScryptP) }
|
||||
}
|
||||
return d, new(d)
|
||||
}
|
||||
139
accounts/keystore/keystore_wallet.go
Normal file
139
accounts/keystore/keystore_wallet.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
ethereum "github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
// keystoreWallet implements the accounts.Wallet interface for the original
|
||||
// keystore.
|
||||
type keystoreWallet struct {
|
||||
account accounts.Account // Single account contained in this wallet
|
||||
keystore *KeyStore // Keystore where the account originates from
|
||||
}
|
||||
|
||||
// URL implements accounts.Wallet, returning the URL of the account within.
|
||||
func (w *keystoreWallet) URL() accounts.URL {
|
||||
return w.account.URL
|
||||
}
|
||||
|
||||
// Status implements accounts.Wallet, always returning "open", since there is no
|
||||
// concept of open/close for plain keystore accounts.
|
||||
func (w *keystoreWallet) Status() string {
|
||||
w.keystore.mu.RLock()
|
||||
defer w.keystore.mu.RUnlock()
|
||||
|
||||
if _, ok := w.keystore.unlocked[w.account.Address]; ok {
|
||||
return "Unlocked"
|
||||
}
|
||||
return "Locked"
|
||||
}
|
||||
|
||||
// Open implements accounts.Wallet, but is a noop for plain wallets since there
|
||||
// is no connection or decryption step necessary to access the list of accounts.
|
||||
func (w *keystoreWallet) Open(passphrase string) error { return nil }
|
||||
|
||||
// Close implements accounts.Wallet, but is a noop for plain wallets since is no
|
||||
// meaningful open operation.
|
||||
func (w *keystoreWallet) Close() error { return nil }
|
||||
|
||||
// Accounts implements accounts.Wallet, returning an account list consisting of
|
||||
// a single account that the plain kestore wallet contains.
|
||||
func (w *keystoreWallet) Accounts() []accounts.Account {
|
||||
return []accounts.Account{w.account}
|
||||
}
|
||||
|
||||
// Contains implements accounts.Wallet, returning whether a particular account is
|
||||
// or is not wrapped by this wallet instance.
|
||||
func (w *keystoreWallet) Contains(account accounts.Account) bool {
|
||||
return account.Address == w.account.Address && (account.URL == (accounts.URL{}) || account.URL == w.account.URL)
|
||||
}
|
||||
|
||||
// Derive implements accounts.Wallet, but is a noop for plain wallets since there
|
||||
// is no notion of hierarchical account derivation for plain keystore accounts.
|
||||
func (w *keystoreWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) {
|
||||
return accounts.Account{}, accounts.ErrNotSupported
|
||||
}
|
||||
|
||||
// SelfDerive implements accounts.Wallet, but is a noop for plain wallets since
|
||||
// there is no notion of hierarchical account derivation for plain keystore accounts.
|
||||
func (w *keystoreWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {}
|
||||
|
||||
// SignHash implements accounts.Wallet, attempting to sign the given hash with
|
||||
// the given account. If the wallet does not wrap this particular account, an
|
||||
// error is returned to avoid account leakage (even though in theory we may be
|
||||
// able to sign via our shared keystore backend).
|
||||
func (w *keystoreWallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) {
|
||||
// Make sure the requested account is contained within
|
||||
if account.Address != w.account.Address {
|
||||
return nil, accounts.ErrUnknownAccount
|
||||
}
|
||||
if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
|
||||
return nil, accounts.ErrUnknownAccount
|
||||
}
|
||||
// Account seems valid, request the keystore to sign
|
||||
return w.keystore.SignHash(account, hash)
|
||||
}
|
||||
|
||||
// SignTx implements accounts.Wallet, attempting to sign the given transaction
|
||||
// with the given account. If the wallet does not wrap this particular account,
|
||||
// an error is returned to avoid account leakage (even though in theory we may
|
||||
// be able to sign via our shared keystore backend).
|
||||
func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||
// Make sure the requested account is contained within
|
||||
if account.Address != w.account.Address {
|
||||
return nil, accounts.ErrUnknownAccount
|
||||
}
|
||||
if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
|
||||
return nil, accounts.ErrUnknownAccount
|
||||
}
|
||||
// Account seems valid, request the keystore to sign
|
||||
return w.keystore.SignTx(account, tx, chainID)
|
||||
}
|
||||
|
||||
// SignHashWithPassphrase implements accounts.Wallet, attempting to sign the
|
||||
// given hash with the given account using passphrase as extra authentication.
|
||||
func (w *keystoreWallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) {
|
||||
// Make sure the requested account is contained within
|
||||
if account.Address != w.account.Address {
|
||||
return nil, accounts.ErrUnknownAccount
|
||||
}
|
||||
if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
|
||||
return nil, accounts.ErrUnknownAccount
|
||||
}
|
||||
// Account seems valid, request the keystore to sign
|
||||
return w.keystore.SignHashWithPassphrase(account, passphrase, hash)
|
||||
}
|
||||
|
||||
// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given
|
||||
// transaction with the given account using passphrase as extra authentication.
|
||||
func (w *keystoreWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||
// Make sure the requested account is contained within
|
||||
if account.Address != w.account.Address {
|
||||
return nil, accounts.ErrUnknownAccount
|
||||
}
|
||||
if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
|
||||
return nil, accounts.ErrUnknownAccount
|
||||
}
|
||||
// Account seems valid, request the keystore to sign
|
||||
return w.keystore.SignTxWithPassphrase(account, passphrase, tx, chainID)
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
@@ -25,20 +25,21 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/pborman/uuid"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
// creates a Key and stores that in the given KeyStore by decrypting a presale key JSON
|
||||
func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (Account, *Key, error) {
|
||||
func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accounts.Account, *Key, error) {
|
||||
key, err := decryptPreSaleKey(keyJSON, password)
|
||||
if err != nil {
|
||||
return Account{}, nil, err
|
||||
return accounts.Account{}, nil, err
|
||||
}
|
||||
key.Id = uuid.NewRandom()
|
||||
a := Account{Address: key.Address, File: keyStore.JoinPath(keyFileName(key.Address))}
|
||||
err = keyStore.StoreKey(a.File, key, password)
|
||||
a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: keyStore.JoinPath(keyFileName(key.Address))}}
|
||||
err = keyStore.StoreKey(a.URL.Path, key, password)
|
||||
return a, key, err
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
// +build darwin,!ios freebsd linux,!arm64 netbsd solaris
|
||||
|
||||
package accounts
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"time"
|
||||
@@ -27,14 +27,14 @@ import (
|
||||
)
|
||||
|
||||
type watcher struct {
|
||||
ac *addrCache
|
||||
ac *accountCache
|
||||
starting bool
|
||||
running bool
|
||||
ev chan notify.EventInfo
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
func newWatcher(ac *addrCache) *watcher {
|
||||
func newWatcher(ac *accountCache) *watcher {
|
||||
return &watcher{
|
||||
ac: ac,
|
||||
ev: make(chan notify.EventInfo, 10),
|
||||
@@ -19,10 +19,10 @@
|
||||
// This is the fallback implementation of directory watching.
|
||||
// It is used on unsupported platforms.
|
||||
|
||||
package accounts
|
||||
package keystore
|
||||
|
||||
type watcher struct{ running bool }
|
||||
|
||||
func newWatcher(*addrCache) *watcher { return new(watcher) }
|
||||
func (*watcher) start() {}
|
||||
func (*watcher) close() {}
|
||||
func newWatcher(*accountCache) *watcher { return new(watcher) }
|
||||
func (*watcher) start() {}
|
||||
func (*watcher) close() {}
|
||||
198
accounts/manager.go
Normal file
198
accounts/manager.go
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
)
|
||||
|
||||
// Manager is an overarching account manager that can communicate with various
|
||||
// backends for signing transactions.
|
||||
type Manager struct {
|
||||
backends map[reflect.Type][]Backend // Index of backends currently registered
|
||||
updaters []event.Subscription // Wallet update subscriptions for all backends
|
||||
updates chan WalletEvent // Subscription sink for backend wallet changes
|
||||
wallets []Wallet // Cache of all wallets from all registered backends
|
||||
|
||||
feed event.Feed // Wallet feed notifying of arrivals/departures
|
||||
|
||||
quit chan chan error
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewManager creates a generic account manager to sign transaction via various
|
||||
// supported backends.
|
||||
func NewManager(backends ...Backend) *Manager {
|
||||
// Subscribe to wallet notifications from all backends
|
||||
updates := make(chan WalletEvent, 4*len(backends))
|
||||
|
||||
subs := make([]event.Subscription, len(backends))
|
||||
for i, backend := range backends {
|
||||
subs[i] = backend.Subscribe(updates)
|
||||
}
|
||||
// Retrieve the initial list of wallets from the backends and sort by URL
|
||||
var wallets []Wallet
|
||||
for _, backend := range backends {
|
||||
wallets = merge(wallets, backend.Wallets()...)
|
||||
}
|
||||
// Assemble the account manager and return
|
||||
am := &Manager{
|
||||
backends: make(map[reflect.Type][]Backend),
|
||||
updaters: subs,
|
||||
updates: updates,
|
||||
wallets: wallets,
|
||||
quit: make(chan chan error),
|
||||
}
|
||||
for _, backend := range backends {
|
||||
kind := reflect.TypeOf(backend)
|
||||
am.backends[kind] = append(am.backends[kind], backend)
|
||||
}
|
||||
go am.update()
|
||||
|
||||
return am
|
||||
}
|
||||
|
||||
// Close terminates the account manager's internal notification processes.
|
||||
func (am *Manager) Close() error {
|
||||
errc := make(chan error)
|
||||
am.quit <- errc
|
||||
return <-errc
|
||||
}
|
||||
|
||||
// update is the wallet event loop listening for notifications from the backends
|
||||
// and updating the cache of wallets.
|
||||
func (am *Manager) update() {
|
||||
// Close all subscriptions when the manager terminates
|
||||
defer func() {
|
||||
am.lock.Lock()
|
||||
for _, sub := range am.updaters {
|
||||
sub.Unsubscribe()
|
||||
}
|
||||
am.updaters = nil
|
||||
am.lock.Unlock()
|
||||
}()
|
||||
|
||||
// Loop until termination
|
||||
for {
|
||||
select {
|
||||
case event := <-am.updates:
|
||||
// Wallet event arrived, update local cache
|
||||
am.lock.Lock()
|
||||
if event.Arrive {
|
||||
am.wallets = merge(am.wallets, event.Wallet)
|
||||
} else {
|
||||
am.wallets = drop(am.wallets, event.Wallet)
|
||||
}
|
||||
am.lock.Unlock()
|
||||
|
||||
// Notify any listeners of the event
|
||||
am.feed.Send(event)
|
||||
|
||||
case errc := <-am.quit:
|
||||
// Manager terminating, return
|
||||
errc <- nil
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backends retrieves the backend(s) with the given type from the account manager.
|
||||
func (am *Manager) Backends(kind reflect.Type) []Backend {
|
||||
return am.backends[kind]
|
||||
}
|
||||
|
||||
// Wallets returns all signer accounts registered under this account manager.
|
||||
func (am *Manager) Wallets() []Wallet {
|
||||
am.lock.RLock()
|
||||
defer am.lock.RUnlock()
|
||||
|
||||
cpy := make([]Wallet, len(am.wallets))
|
||||
copy(cpy, am.wallets)
|
||||
return cpy
|
||||
}
|
||||
|
||||
// Wallet retrieves the wallet associated with a particular URL.
|
||||
func (am *Manager) Wallet(url string) (Wallet, error) {
|
||||
am.lock.RLock()
|
||||
defer am.lock.RUnlock()
|
||||
|
||||
parsed, err := parseURL(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, wallet := range am.Wallets() {
|
||||
if wallet.URL() == parsed {
|
||||
return wallet, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrUnknownWallet
|
||||
}
|
||||
|
||||
// Find attempts to locate the wallet corresponding to a specific account. Since
|
||||
// accounts can be dynamically added to and removed from wallets, this method has
|
||||
// a linear runtime in the number of wallets.
|
||||
func (am *Manager) Find(account Account) (Wallet, error) {
|
||||
am.lock.RLock()
|
||||
defer am.lock.RUnlock()
|
||||
|
||||
for _, wallet := range am.wallets {
|
||||
if wallet.Contains(account) {
|
||||
return wallet, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrUnknownAccount
|
||||
}
|
||||
|
||||
// Subscribe creates an async subscription to receive notifications when the
|
||||
// manager detects the arrival or departure of a wallet from any of its backends.
|
||||
func (am *Manager) Subscribe(sink chan<- WalletEvent) event.Subscription {
|
||||
return am.feed.Subscribe(sink)
|
||||
}
|
||||
|
||||
// merge is a sorted analogue of append for wallets, where the ordering of the
|
||||
// origin list is preserved by inserting new wallets at the correct position.
|
||||
//
|
||||
// The original slice is assumed to be already sorted by URL.
|
||||
func merge(slice []Wallet, wallets ...Wallet) []Wallet {
|
||||
for _, wallet := range wallets {
|
||||
n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 })
|
||||
if n == len(slice) {
|
||||
slice = append(slice, wallet)
|
||||
continue
|
||||
}
|
||||
slice = append(slice[:n], append([]Wallet{wallet}, slice[n:]...)...)
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// drop is the couterpart of merge, which looks up wallets from within the sorted
|
||||
// cache and removes the ones specified.
|
||||
func drop(slice []Wallet, wallets ...Wallet) []Wallet {
|
||||
for _, wallet := range wallets {
|
||||
n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 })
|
||||
if n == len(slice) {
|
||||
// Wallet not found, may happen during startup
|
||||
continue
|
||||
}
|
||||
slice = append(slice[:n], slice[n+1:]...)
|
||||
}
|
||||
return slice
|
||||
}
|
||||
79
accounts/url.go
Normal file
79
accounts/url.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// URL represents the canonical identification URL of a wallet or account.
|
||||
//
|
||||
// It is a simplified version of url.URL, with the important limitations (which
|
||||
// are considered features here) that it contains value-copyable components only,
|
||||
// as well as that it doesn't do any URL encoding/decoding of special characters.
|
||||
//
|
||||
// The former is important to allow an account to be copied without leaving live
|
||||
// references to the original version, whereas the latter is important to ensure
|
||||
// one single canonical form opposed to many allowed ones by the RFC 3986 spec.
|
||||
//
|
||||
// As such, these URLs should not be used outside of the scope of an Ethereum
|
||||
// wallet or account.
|
||||
type URL struct {
|
||||
Scheme string // Protocol scheme to identify a capable account backend
|
||||
Path string // Path for the backend to identify a unique entity
|
||||
}
|
||||
|
||||
// parseURL converts a user supplied URL into the accounts specific structure.
|
||||
func parseURL(url string) (URL, error) {
|
||||
parts := strings.Split(url, "://")
|
||||
if len(parts) != 2 || parts[0] == "" {
|
||||
return URL{}, errors.New("protocol scheme missing")
|
||||
}
|
||||
return URL{
|
||||
Scheme: parts[0],
|
||||
Path: parts[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// String implements the stringer interface.
|
||||
func (u URL) String() string {
|
||||
if u.Scheme != "" {
|
||||
return fmt.Sprintf("%s://%s", u.Scheme, u.Path)
|
||||
}
|
||||
return u.Path
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaller interface.
|
||||
func (u URL) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(u.String())
|
||||
}
|
||||
|
||||
// Cmp compares x and y and returns:
|
||||
//
|
||||
// -1 if x < y
|
||||
// 0 if x == y
|
||||
// +1 if x > y
|
||||
//
|
||||
func (u URL) Cmp(url URL) int {
|
||||
if u.Scheme == url.Scheme {
|
||||
return strings.Compare(u.Path, url.Path)
|
||||
}
|
||||
return strings.Compare(u.Scheme, url.Scheme)
|
||||
}
|
||||
209
accounts/usbwallet/ledger_hub.go
Normal file
209
accounts/usbwallet/ledger_hub.go
Normal file
@@ -0,0 +1,209 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// This file contains the implementation for interacting with the Ledger hardware
|
||||
// wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo:
|
||||
// https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc
|
||||
|
||||
// +build !ios
|
||||
|
||||
package usbwallet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/karalabe/gousb/usb"
|
||||
)
|
||||
|
||||
// LedgerScheme is the protocol scheme prefixing account and wallet URLs.
|
||||
var LedgerScheme = "ledger"
|
||||
|
||||
// ledgerDeviceIDs are the known device IDs that Ledger wallets use.
|
||||
var ledgerDeviceIDs = []deviceID{
|
||||
{Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue
|
||||
{Vendor: 0x2c97, Product: 0x0001}, // Ledger Nano S
|
||||
}
|
||||
|
||||
// Maximum time between wallet refreshes (if USB hotplug notifications don't work).
|
||||
const ledgerRefreshCycle = time.Second
|
||||
|
||||
// Minimum time between wallet refreshes to avoid USB trashing.
|
||||
const ledgerRefreshThrottling = 500 * time.Millisecond
|
||||
|
||||
// LedgerHub is a accounts.Backend that can find and handle Ledger hardware wallets.
|
||||
type LedgerHub struct {
|
||||
ctx *usb.Context // Context interfacing with a libusb instance
|
||||
|
||||
refreshed time.Time // Time instance when the list of wallets was last refreshed
|
||||
wallets []accounts.Wallet // List of Ledger devices currently tracking
|
||||
updateFeed event.Feed // Event feed to notify wallet additions/removals
|
||||
updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
|
||||
updating bool // Whether the event notification loop is running
|
||||
|
||||
quit chan chan error
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewLedgerHub creates a new hardware wallet manager for Ledger devices.
|
||||
func NewLedgerHub() (*LedgerHub, error) {
|
||||
// Initialize the USB library to access Ledgers through
|
||||
ctx, err := usb.NewContext()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Create the USB hub, start and return it
|
||||
hub := &LedgerHub{
|
||||
ctx: ctx,
|
||||
quit: make(chan chan error),
|
||||
}
|
||||
hub.refreshWallets()
|
||||
|
||||
return hub, nil
|
||||
}
|
||||
|
||||
// Wallets implements accounts.Backend, returning all the currently tracked USB
|
||||
// devices that appear to be Ledger hardware wallets.
|
||||
func (hub *LedgerHub) Wallets() []accounts.Wallet {
|
||||
// Make sure the list of wallets is up to date
|
||||
hub.refreshWallets()
|
||||
|
||||
hub.lock.RLock()
|
||||
defer hub.lock.RUnlock()
|
||||
|
||||
cpy := make([]accounts.Wallet, len(hub.wallets))
|
||||
copy(cpy, hub.wallets)
|
||||
return cpy
|
||||
}
|
||||
|
||||
// refreshWallets scans the USB devices attached to the machine and updates the
|
||||
// list of wallets based on the found devices.
|
||||
func (hub *LedgerHub) refreshWallets() {
|
||||
// Don't scan the USB like crazy it the user fetches wallets in a loop
|
||||
hub.lock.RLock()
|
||||
elapsed := time.Since(hub.refreshed)
|
||||
hub.lock.RUnlock()
|
||||
|
||||
if elapsed < ledgerRefreshThrottling {
|
||||
return
|
||||
}
|
||||
// Retrieve the current list of Ledger devices
|
||||
var devIDs []deviceID
|
||||
var busIDs []uint16
|
||||
|
||||
hub.ctx.ListDevices(func(desc *usb.Descriptor) bool {
|
||||
// Gather Ledger devices, don't connect any just yet
|
||||
for _, id := range ledgerDeviceIDs {
|
||||
if desc.Vendor == id.Vendor && desc.Product == id.Product {
|
||||
devIDs = append(devIDs, deviceID{Vendor: desc.Vendor, Product: desc.Product})
|
||||
busIDs = append(busIDs, uint16(desc.Bus)<<8+uint16(desc.Address))
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Not ledger, ignore and don't connect either
|
||||
return false
|
||||
})
|
||||
// Transform the current list of wallets into the new one
|
||||
hub.lock.Lock()
|
||||
|
||||
wallets := make([]accounts.Wallet, 0, len(devIDs))
|
||||
events := []accounts.WalletEvent{}
|
||||
|
||||
for i := 0; i < len(devIDs); i++ {
|
||||
devID, busID := devIDs[i], busIDs[i]
|
||||
|
||||
url := accounts.URL{Scheme: LedgerScheme, Path: fmt.Sprintf("%03d:%03d", busID>>8, busID&0xff)}
|
||||
|
||||
// Drop wallets in front of the next device or those that failed for some reason
|
||||
for len(hub.wallets) > 0 && (hub.wallets[0].URL().Cmp(url) < 0 || hub.wallets[0].(*ledgerWallet).failed()) {
|
||||
events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false})
|
||||
hub.wallets = hub.wallets[1:]
|
||||
}
|
||||
// If there are no more wallets or the device is before the next, wrap new wallet
|
||||
if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 {
|
||||
wallet := &ledgerWallet{context: hub.ctx, hardwareID: devID, locationID: busID, url: &url}
|
||||
|
||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true})
|
||||
wallets = append(wallets, wallet)
|
||||
continue
|
||||
}
|
||||
// If the device is the same as the first wallet, keep it
|
||||
if hub.wallets[0].URL().Cmp(url) == 0 {
|
||||
wallets = append(wallets, hub.wallets[0])
|
||||
hub.wallets = hub.wallets[1:]
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Drop any leftover wallets and set the new batch
|
||||
for _, wallet := range hub.wallets {
|
||||
events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false})
|
||||
}
|
||||
hub.refreshed = time.Now()
|
||||
hub.wallets = wallets
|
||||
hub.lock.Unlock()
|
||||
|
||||
// Fire all wallet events and return
|
||||
for _, event := range events {
|
||||
hub.updateFeed.Send(event)
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe implements accounts.Backend, creating an async subscription to
|
||||
// receive notifications on the addition or removal of Ledger wallets.
|
||||
func (hub *LedgerHub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
|
||||
// We need the mutex to reliably start/stop the update loop
|
||||
hub.lock.Lock()
|
||||
defer hub.lock.Unlock()
|
||||
|
||||
// Subscribe the caller and track the subscriber count
|
||||
sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink))
|
||||
|
||||
// Subscribers require an active notification loop, start it
|
||||
if !hub.updating {
|
||||
hub.updating = true
|
||||
go hub.updater()
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
// updater is responsible for maintaining an up-to-date list of wallets stored in
|
||||
// the keystore, and for firing wallet addition/removal events. It listens for
|
||||
// account change events from the underlying account cache, and also periodically
|
||||
// forces a manual refresh (only triggers for systems where the filesystem notifier
|
||||
// is not running).
|
||||
func (hub *LedgerHub) updater() {
|
||||
for {
|
||||
// Wait for a USB hotplug event (not supported yet) or a refresh timeout
|
||||
select {
|
||||
//case <-hub.changes: // reenable on hutplug implementation
|
||||
case <-time.After(ledgerRefreshCycle):
|
||||
}
|
||||
// Run the wallet refresher
|
||||
hub.refreshWallets()
|
||||
|
||||
// If all our subscribers left, stop the updater
|
||||
hub.lock.Lock()
|
||||
if hub.updateScope.Count() == 0 {
|
||||
hub.updating = false
|
||||
hub.lock.Unlock()
|
||||
return
|
||||
}
|
||||
hub.lock.Unlock()
|
||||
}
|
||||
}
|
||||
945
accounts/usbwallet/ledger_wallet.go
Normal file
945
accounts/usbwallet/ledger_wallet.go
Normal file
@@ -0,0 +1,945 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// This file contains the implementation for interacting with the Ledger hardware
|
||||
// wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo:
|
||||
// https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc
|
||||
|
||||
// +build !ios
|
||||
|
||||
package usbwallet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
ethereum "github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/karalabe/gousb/usb"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Maximum time between wallet health checks to detect USB unplugs.
|
||||
const ledgerHeartbeatCycle = time.Second
|
||||
|
||||
// Minimum time to wait between self derivation attempts, even it the user is
|
||||
// requesting accounts like crazy.
|
||||
const ledgerSelfDeriveThrottling = time.Second
|
||||
|
||||
// ledgerOpcode is an enumeration encoding the supported Ledger opcodes.
|
||||
type ledgerOpcode byte
|
||||
|
||||
// ledgerParam1 is an enumeration encoding the supported Ledger parameters for
|
||||
// specific opcodes. The same parameter values may be reused between opcodes.
|
||||
type ledgerParam1 byte
|
||||
|
||||
// ledgerParam2 is an enumeration encoding the supported Ledger parameters for
|
||||
// specific opcodes. The same parameter values may be reused between opcodes.
|
||||
type ledgerParam2 byte
|
||||
|
||||
const (
|
||||
ledgerOpRetrieveAddress ledgerOpcode = 0x02 // Returns the public key and Ethereum address for a given BIP 32 path
|
||||
ledgerOpSignTransaction ledgerOpcode = 0x04 // Signs an Ethereum transaction after having the user validate the parameters
|
||||
ledgerOpGetConfiguration ledgerOpcode = 0x06 // Returns specific wallet application configuration
|
||||
|
||||
ledgerP1DirectlyFetchAddress ledgerParam1 = 0x00 // Return address directly from the wallet
|
||||
ledgerP1ConfirmFetchAddress ledgerParam1 = 0x01 // Require a user confirmation before returning the address
|
||||
ledgerP1InitTransactionData ledgerParam1 = 0x00 // First transaction data block for signing
|
||||
ledgerP1ContTransactionData ledgerParam1 = 0x80 // Subsequent transaction data block for signing
|
||||
ledgerP2DiscardAddressChainCode ledgerParam2 = 0x00 // Do not return the chain code along with the address
|
||||
ledgerP2ReturnAddressChainCode ledgerParam2 = 0x01 // Require a user confirmation before returning the address
|
||||
)
|
||||
|
||||
// errReplyInvalidHeader is the error message returned by a Ledfer data exchange
|
||||
// if the device replies with a mismatching header. This usually means the device
|
||||
// is in browser mode.
|
||||
var errReplyInvalidHeader = errors.New("invalid reply header")
|
||||
|
||||
// ledgerWallet represents a live USB Ledger hardware wallet.
|
||||
type ledgerWallet struct {
|
||||
context *usb.Context // USB context to interface libusb through
|
||||
hardwareID deviceID // USB identifiers to identify this device type
|
||||
locationID uint16 // USB bus and address to identify this device instance
|
||||
url *accounts.URL // Textual URL uniquely identifying this wallet
|
||||
|
||||
device *usb.Device // USB device advertising itself as a Ledger wallet
|
||||
input usb.Endpoint // Input endpoint to send data to this device
|
||||
output usb.Endpoint // Output endpoint to receive data from this device
|
||||
failure error // Any failure that would make the device unusable
|
||||
|
||||
version [3]byte // Current version of the Ledger Ethereum app (zero if app is offline)
|
||||
browser bool // Flag whether the Ledger is in browser mode (reply channel mismatch)
|
||||
accounts []accounts.Account // List of derive accounts pinned on the Ledger
|
||||
paths map[common.Address]accounts.DerivationPath // Known derivation paths for signing operations
|
||||
|
||||
deriveNextPath accounts.DerivationPath // Next derivation path for account auto-discovery
|
||||
deriveNextAddr common.Address // Next derived account address for auto-discovery
|
||||
deriveChain ethereum.ChainStateReader // Blockchain state reader to discover used account with
|
||||
deriveReq chan chan struct{} // Channel to request a self-derivation on
|
||||
deriveQuit chan chan error // Channel to terminate the self-deriver with
|
||||
|
||||
healthQuit chan chan error
|
||||
|
||||
// Locking a hardware wallet is a bit special. Since hardware devices are lower
|
||||
// performing, any communication with them might take a non negligible amount of
|
||||
// time. Worse still, waiting for user confirmation can take arbitrarily long,
|
||||
// but exclusive communication must be upheld during. Locking the entire wallet
|
||||
// in the mean time however would stall any parts of the system that don't want
|
||||
// to communicate, just read some state (e.g. list the accounts).
|
||||
//
|
||||
// As such, a hardware wallet needs two locks to function correctly. A state
|
||||
// lock can be used to protect the wallet's software-side internal state, which
|
||||
// must not be held exlusively during hardware communication. A communication
|
||||
// lock can be used to achieve exclusive access to the device itself, this one
|
||||
// however should allow "skipping" waiting for operations that might want to
|
||||
// use the device, but can live without too (e.g. account self-derivation).
|
||||
//
|
||||
// Since we have two locks, it's important to know how to properly use them:
|
||||
// - Communication requires the `device` to not change, so obtaining the
|
||||
// commsLock should be done after having a stateLock.
|
||||
// - Communication must not disable read access to the wallet state, so it
|
||||
// must only ever hold a *read* lock to stateLock.
|
||||
commsLock chan struct{} // Mutex (buf=1) for the USB comms without keeping the state locked
|
||||
stateLock sync.RWMutex // Protects read and write access to the wallet struct fields
|
||||
}
|
||||
|
||||
// URL implements accounts.Wallet, returning the URL of the Ledger device.
|
||||
func (w *ledgerWallet) URL() accounts.URL {
|
||||
return *w.url // Immutable, no need for a lock
|
||||
}
|
||||
|
||||
// Status implements accounts.Wallet, always whether the Ledger is opened, closed
|
||||
// or whether the Ethereum app was not started on it.
|
||||
func (w *ledgerWallet) Status() string {
|
||||
w.stateLock.RLock() // No device communication, state lock is enough
|
||||
defer w.stateLock.RUnlock()
|
||||
|
||||
if w.failure != nil {
|
||||
return fmt.Sprintf("Failed: %v", w.failure)
|
||||
}
|
||||
if w.device == nil {
|
||||
return "Closed"
|
||||
}
|
||||
if w.browser {
|
||||
return "Ethereum app in browser mode"
|
||||
}
|
||||
if w.offline() {
|
||||
return "Ethereum app offline"
|
||||
}
|
||||
return fmt.Sprintf("Ethereum app v%d.%d.%d online", w.version[0], w.version[1], w.version[2])
|
||||
}
|
||||
|
||||
// offline returns whether the wallet and the Ethereum app is offline or not.
|
||||
//
|
||||
// The method assumes that the state lock is held!
|
||||
func (w *ledgerWallet) offline() bool {
|
||||
return w.version == [3]byte{0, 0, 0}
|
||||
}
|
||||
|
||||
// failed returns if the USB device wrapped by the wallet failed for some reason.
|
||||
// This is used by the device scanner to report failed wallets as departed.
|
||||
//
|
||||
// The method assumes that the state lock is *not* held!
|
||||
func (w *ledgerWallet) failed() bool {
|
||||
w.stateLock.RLock() // No device communication, state lock is enough
|
||||
defer w.stateLock.RUnlock()
|
||||
|
||||
return w.failure != nil
|
||||
}
|
||||
|
||||
// Open implements accounts.Wallet, attempting to open a USB connection to the
|
||||
// Ledger hardware wallet. The Ledger does not require a user passphrase, so that
|
||||
// parameter is silently discarded.
|
||||
func (w *ledgerWallet) Open(passphrase string) error {
|
||||
w.stateLock.Lock() // State lock is enough since there's no connection yet at this point
|
||||
defer w.stateLock.Unlock()
|
||||
|
||||
// If the wallet was already opened, don't try to open again
|
||||
if w.device != nil {
|
||||
return accounts.ErrWalletAlreadyOpen
|
||||
}
|
||||
// Otherwise iterate over all USB devices and find this again (no way to directly do this)
|
||||
// Iterate over all attached devices and fetch those seemingly Ledger
|
||||
devices, err := w.context.ListDevices(func(desc *usb.Descriptor) bool {
|
||||
// Only open this single specific device
|
||||
return desc.Vendor == w.hardwareID.Vendor && desc.Product == w.hardwareID.Product &&
|
||||
uint16(desc.Bus)<<8+uint16(desc.Address) == w.locationID
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
return accounts.ErrUnknownWallet
|
||||
}
|
||||
// Device opened, attach to the input and output endpoints
|
||||
device := devices[0]
|
||||
|
||||
var invalid string
|
||||
switch {
|
||||
case len(device.Descriptor.Configs) == 0:
|
||||
invalid = "no endpoint config available"
|
||||
case len(device.Descriptor.Configs[0].Interfaces) == 0:
|
||||
invalid = "no endpoint interface available"
|
||||
case len(device.Descriptor.Configs[0].Interfaces[0].Setups) == 0:
|
||||
invalid = "no endpoint setup available"
|
||||
case len(device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints) < 2:
|
||||
invalid = "not enough IO endpoints available"
|
||||
}
|
||||
if invalid != "" {
|
||||
device.Close()
|
||||
return fmt.Errorf("ledger wallet [%s] invalid: %s", w.url, invalid)
|
||||
}
|
||||
// Open the input and output endpoints to the device
|
||||
input, err := device.OpenEndpoint(
|
||||
device.Descriptor.Configs[0].Config,
|
||||
device.Descriptor.Configs[0].Interfaces[0].Number,
|
||||
device.Descriptor.Configs[0].Interfaces[0].Setups[0].Number,
|
||||
device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints[1].Address,
|
||||
)
|
||||
if err != nil {
|
||||
device.Close()
|
||||
return fmt.Errorf("ledger wallet [%s] input open failed: %v", w.url, err)
|
||||
}
|
||||
output, err := device.OpenEndpoint(
|
||||
device.Descriptor.Configs[0].Config,
|
||||
device.Descriptor.Configs[0].Interfaces[0].Number,
|
||||
device.Descriptor.Configs[0].Interfaces[0].Setups[0].Number,
|
||||
device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints[0].Address,
|
||||
)
|
||||
if err != nil {
|
||||
device.Close()
|
||||
return fmt.Errorf("ledger wallet [%s] output open failed: %v", w.url, err)
|
||||
}
|
||||
// Wallet seems to be successfully opened, guess if the Ethereum app is running
|
||||
w.device, w.input, w.output = device, input, output
|
||||
w.commsLock = make(chan struct{}, 1)
|
||||
w.commsLock <- struct{}{} // Enable lock
|
||||
|
||||
w.paths = make(map[common.Address]accounts.DerivationPath)
|
||||
|
||||
w.deriveReq = make(chan chan struct{})
|
||||
w.deriveQuit = make(chan chan error)
|
||||
w.healthQuit = make(chan chan error)
|
||||
|
||||
defer func() {
|
||||
go w.heartbeat()
|
||||
go w.selfDerive()
|
||||
}()
|
||||
|
||||
if _, err = w.ledgerDerive(accounts.DefaultBaseDerivationPath); err != nil {
|
||||
// Ethereum app is not running or in browser mode, nothing more to do, return
|
||||
if err == errReplyInvalidHeader {
|
||||
w.browser = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Try to resolve the Ethereum app's version, will fail prior to v1.0.2
|
||||
if w.version, err = w.ledgerVersion(); err != nil {
|
||||
w.version = [3]byte{1, 0, 0} // Assume worst case, can't verify if v1.0.0 or v1.0.1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// heartbeat is a health check loop for the Ledger wallets to periodically verify
|
||||
// whether they are still present or if they malfunctioned. It is needed because:
|
||||
// - libusb on Windows doesn't support hotplug, so we can't detect USB unplugs
|
||||
// - communication timeout on the Ledger requires a device power cycle to fix
|
||||
func (w *ledgerWallet) heartbeat() {
|
||||
glog.V(logger.Debug).Infof("%s health-check started", w.url.String())
|
||||
defer glog.V(logger.Debug).Infof("%s health-check stopped", w.url.String())
|
||||
|
||||
// Execute heartbeat checks until termination or error
|
||||
var (
|
||||
errc chan error
|
||||
err error
|
||||
)
|
||||
for errc == nil && err == nil {
|
||||
// Wait until termination is requested or the heartbeat cycle arrives
|
||||
select {
|
||||
case errc = <-w.healthQuit:
|
||||
// Termination requested
|
||||
continue
|
||||
case <-time.After(ledgerHeartbeatCycle):
|
||||
// Heartbeat time
|
||||
}
|
||||
// Execute a tiny data exchange to see responsiveness
|
||||
w.stateLock.RLock()
|
||||
if w.device == nil {
|
||||
// Terminated while waiting for the lock
|
||||
w.stateLock.RUnlock()
|
||||
continue
|
||||
}
|
||||
<-w.commsLock // Don't lock state while resolving version
|
||||
_, err = w.ledgerVersion()
|
||||
w.commsLock <- struct{}{}
|
||||
w.stateLock.RUnlock()
|
||||
|
||||
if err == usb.ERROR_IO || err == usb.ERROR_NO_DEVICE {
|
||||
w.stateLock.Lock() // Lock state to tear the wallet down
|
||||
w.failure = err
|
||||
w.close()
|
||||
w.stateLock.Unlock()
|
||||
}
|
||||
// Ignore uninteresting errors
|
||||
err = nil
|
||||
}
|
||||
// In case of error, wait for termination
|
||||
if err != nil {
|
||||
glog.V(logger.Debug).Infof("%s health-check failed: %v", w.url.String(), err)
|
||||
errc = <-w.healthQuit
|
||||
}
|
||||
errc <- err
|
||||
}
|
||||
|
||||
// Close implements accounts.Wallet, closing the USB connection to the Ledger.
|
||||
func (w *ledgerWallet) Close() error {
|
||||
// Ensure the wallet was opened
|
||||
w.stateLock.RLock()
|
||||
hQuit, dQuit := w.healthQuit, w.deriveQuit
|
||||
w.stateLock.RUnlock()
|
||||
|
||||
// Terminate the health checks
|
||||
var herr error
|
||||
if hQuit != nil {
|
||||
errc := make(chan error)
|
||||
hQuit <- errc
|
||||
herr = <-errc // Save for later, we *must* close the USB
|
||||
}
|
||||
// Terminate the self-derivations
|
||||
var derr error
|
||||
if dQuit != nil {
|
||||
errc := make(chan error)
|
||||
dQuit <- errc
|
||||
derr = <-errc // Save for later, we *must* close the USB
|
||||
}
|
||||
// Terminate the device connection
|
||||
w.stateLock.Lock()
|
||||
defer w.stateLock.Unlock()
|
||||
|
||||
w.healthQuit = nil
|
||||
w.deriveQuit = nil
|
||||
w.deriveReq = nil
|
||||
|
||||
if err := w.close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if herr != nil {
|
||||
return herr
|
||||
}
|
||||
return derr
|
||||
}
|
||||
|
||||
// close is the internal wallet closer that terminates the USB connection and
|
||||
// resets all the fields to their defaults.
|
||||
//
|
||||
// Note, close assumes the state lock is held!
|
||||
func (w *ledgerWallet) close() error {
|
||||
// Allow duplicate closes, especially for health-check failures
|
||||
if w.device == nil {
|
||||
return nil
|
||||
}
|
||||
// Close the device, clear everything, then return
|
||||
err := w.device.Close()
|
||||
|
||||
w.device, w.input, w.output = nil, nil, nil
|
||||
w.browser, w.version = false, [3]byte{}
|
||||
w.accounts, w.paths = nil, nil
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Accounts implements accounts.Wallet, returning the list of accounts pinned to
|
||||
// the Ledger hardware wallet. If self-derivation was enabled, the account list
|
||||
// is periodically expanded based on current chain state.
|
||||
func (w *ledgerWallet) Accounts() []accounts.Account {
|
||||
// Attempt self-derivation if it's running
|
||||
reqc := make(chan struct{}, 1)
|
||||
select {
|
||||
case w.deriveReq <- reqc:
|
||||
// Self-derivation request accepted, wait for it
|
||||
<-reqc
|
||||
default:
|
||||
// Self-derivation offline, throttled or busy, skip
|
||||
}
|
||||
// Return whatever account list we ended up with
|
||||
w.stateLock.RLock()
|
||||
defer w.stateLock.RUnlock()
|
||||
|
||||
cpy := make([]accounts.Account, len(w.accounts))
|
||||
copy(cpy, w.accounts)
|
||||
return cpy
|
||||
}
|
||||
|
||||
// selfDerive is an account derivation loop that upon request attempts to find
|
||||
// new non-zero accounts.
|
||||
func (w *ledgerWallet) selfDerive() {
|
||||
glog.V(logger.Debug).Infof("%s self-derivation started", w.url.String())
|
||||
defer glog.V(logger.Debug).Infof("%s self-derivation stopped", w.url.String())
|
||||
|
||||
// Execute self-derivations until termination or error
|
||||
var (
|
||||
reqc chan struct{}
|
||||
errc chan error
|
||||
err error
|
||||
)
|
||||
for errc == nil && err == nil {
|
||||
// Wait until either derivation or termination is requested
|
||||
select {
|
||||
case errc = <-w.deriveQuit:
|
||||
// Termination requested
|
||||
continue
|
||||
case reqc = <-w.deriveReq:
|
||||
// Account discovery requested
|
||||
}
|
||||
// Derivation needs a chain and device access, skip if either unavailable
|
||||
w.stateLock.RLock()
|
||||
if w.device == nil || w.deriveChain == nil || w.offline() {
|
||||
w.stateLock.RUnlock()
|
||||
reqc <- struct{}{}
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-w.commsLock:
|
||||
default:
|
||||
w.stateLock.RUnlock()
|
||||
reqc <- struct{}{}
|
||||
continue
|
||||
}
|
||||
// Device lock obtained, derive the next batch of accounts
|
||||
var (
|
||||
accs []accounts.Account
|
||||
paths []accounts.DerivationPath
|
||||
|
||||
nextAddr = w.deriveNextAddr
|
||||
nextPath = w.deriveNextPath
|
||||
|
||||
context = context.Background()
|
||||
)
|
||||
for empty := false; !empty; {
|
||||
// Retrieve the next derived Ethereum account
|
||||
if nextAddr == (common.Address{}) {
|
||||
if nextAddr, err = w.ledgerDerive(nextPath); err != nil {
|
||||
glog.V(logger.Warn).Infof("%s self-derivation failed: %v", w.url.String(), err)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Check the account's status against the current chain state
|
||||
var (
|
||||
balance *big.Int
|
||||
nonce uint64
|
||||
)
|
||||
balance, err = w.deriveChain.BalanceAt(context, nextAddr, nil)
|
||||
if err != nil {
|
||||
glog.V(logger.Warn).Infof("%s self-derivation balance retrieval failed: %v", w.url.String(), err)
|
||||
break
|
||||
}
|
||||
nonce, err = w.deriveChain.NonceAt(context, nextAddr, nil)
|
||||
if err != nil {
|
||||
glog.V(logger.Warn).Infof("%s self-derivation nonce retrieval failed: %v", w.url.String(), err)
|
||||
break
|
||||
}
|
||||
// If the next account is empty, stop self-derivation, but add it nonetheless
|
||||
if balance.BitLen() == 0 && nonce == 0 {
|
||||
empty = true
|
||||
}
|
||||
// We've just self-derived a new account, start tracking it locally
|
||||
path := make(accounts.DerivationPath, len(nextPath))
|
||||
copy(path[:], nextPath[:])
|
||||
paths = append(paths, path)
|
||||
|
||||
account := accounts.Account{
|
||||
Address: nextAddr,
|
||||
URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)},
|
||||
}
|
||||
accs = append(accs, account)
|
||||
|
||||
// Display a log message to the user for new (or previously empty accounts)
|
||||
if _, known := w.paths[nextAddr]; !known || (!empty && nextAddr == w.deriveNextAddr) {
|
||||
glog.V(logger.Info).Infof("%s discovered %s (balance %22v, nonce %4d) at %s", w.url.String(), nextAddr.Hex(), balance, nonce, path)
|
||||
}
|
||||
// Fetch the next potential account
|
||||
if !empty {
|
||||
nextAddr = common.Address{}
|
||||
nextPath[len(nextPath)-1]++
|
||||
}
|
||||
}
|
||||
// Self derivation complete, release device lock
|
||||
w.commsLock <- struct{}{}
|
||||
w.stateLock.RUnlock()
|
||||
|
||||
// Insert any accounts successfully derived
|
||||
w.stateLock.Lock()
|
||||
for i := 0; i < len(accs); i++ {
|
||||
if _, ok := w.paths[accs[i].Address]; !ok {
|
||||
w.accounts = append(w.accounts, accs[i])
|
||||
w.paths[accs[i].Address] = paths[i]
|
||||
}
|
||||
}
|
||||
// Shift the self-derivation forward
|
||||
// TODO(karalabe): don't overwrite changes from wallet.SelfDerive
|
||||
w.deriveNextAddr = nextAddr
|
||||
w.deriveNextPath = nextPath
|
||||
w.stateLock.Unlock()
|
||||
|
||||
// Notify the user of termination and loop after a bit of time (to avoid trashing)
|
||||
reqc <- struct{}{}
|
||||
if err == nil {
|
||||
select {
|
||||
case errc = <-w.deriveQuit:
|
||||
// Termination requested, abort
|
||||
case <-time.After(ledgerSelfDeriveThrottling):
|
||||
// Waited enough, willing to self-derive again
|
||||
}
|
||||
}
|
||||
}
|
||||
// In case of error, wait for termination
|
||||
if err != nil {
|
||||
glog.V(logger.Debug).Infof("%s self-derivation failed: %s", w.url.String(), err)
|
||||
errc = <-w.deriveQuit
|
||||
}
|
||||
errc <- err
|
||||
}
|
||||
|
||||
// Contains implements accounts.Wallet, returning whether a particular account is
|
||||
// or is not pinned into this Ledger instance. Although we could attempt to resolve
|
||||
// unpinned accounts, that would be an non-negligible hardware operation.
|
||||
func (w *ledgerWallet) Contains(account accounts.Account) bool {
|
||||
w.stateLock.RLock()
|
||||
defer w.stateLock.RUnlock()
|
||||
|
||||
_, exists := w.paths[account.Address]
|
||||
return exists
|
||||
}
|
||||
|
||||
// Derive implements accounts.Wallet, deriving a new account at the specific
|
||||
// derivation path. If pin is set to true, the account will be added to the list
|
||||
// of tracked accounts.
|
||||
func (w *ledgerWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) {
|
||||
// Try to derive the actual account and update its URL if successful
|
||||
w.stateLock.RLock() // Avoid device disappearing during derivation
|
||||
|
||||
if w.device == nil || w.offline() {
|
||||
w.stateLock.RUnlock()
|
||||
return accounts.Account{}, accounts.ErrWalletClosed
|
||||
}
|
||||
<-w.commsLock // Avoid concurrent hardware access
|
||||
address, err := w.ledgerDerive(path)
|
||||
w.commsLock <- struct{}{}
|
||||
|
||||
w.stateLock.RUnlock()
|
||||
|
||||
// If an error occurred or no pinning was requested, return
|
||||
if err != nil {
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
account := accounts.Account{
|
||||
Address: address,
|
||||
URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)},
|
||||
}
|
||||
if !pin {
|
||||
return account, nil
|
||||
}
|
||||
// Pinning needs to modify the state
|
||||
w.stateLock.Lock()
|
||||
defer w.stateLock.Unlock()
|
||||
|
||||
if _, ok := w.paths[address]; !ok {
|
||||
w.accounts = append(w.accounts, account)
|
||||
w.paths[address] = path
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// SelfDerive implements accounts.Wallet, trying to discover accounts that the
|
||||
// user used previously (based on the chain state), but ones that he/she did not
|
||||
// explicitly pin to the wallet manually. To avoid chain head monitoring, self
|
||||
// derivation only runs during account listing (and even then throttled).
|
||||
func (w *ledgerWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {
|
||||
w.stateLock.Lock()
|
||||
defer w.stateLock.Unlock()
|
||||
|
||||
w.deriveNextPath = make(accounts.DerivationPath, len(base))
|
||||
copy(w.deriveNextPath[:], base[:])
|
||||
|
||||
w.deriveNextAddr = common.Address{}
|
||||
w.deriveChain = chain
|
||||
}
|
||||
|
||||
// SignHash implements accounts.Wallet, however signing arbitrary data is not
|
||||
// supported for Ledger wallets, so this method will always return an error.
|
||||
func (w *ledgerWallet) SignHash(acc accounts.Account, hash []byte) ([]byte, error) {
|
||||
return nil, accounts.ErrNotSupported
|
||||
}
|
||||
|
||||
// SignTx implements accounts.Wallet. It sends the transaction over to the Ledger
|
||||
// wallet to request a confirmation from the user. It returns either the signed
|
||||
// transaction or a failure if the user denied the transaction.
|
||||
//
|
||||
// Note, if the version of the Ethereum application running on the Ledger wallet is
|
||||
// too old to sign EIP-155 transactions, but such is requested nonetheless, an error
|
||||
// will be returned opposed to silently signing in Homestead mode.
|
||||
func (w *ledgerWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||
w.stateLock.RLock() // Comms have own mutex, this is for the state fields
|
||||
defer w.stateLock.RUnlock()
|
||||
|
||||
// If the wallet is closed, or the Ethereum app doesn't run, abort
|
||||
if w.device == nil || w.offline() {
|
||||
return nil, accounts.ErrWalletClosed
|
||||
}
|
||||
// Make sure the requested account is contained within
|
||||
path, ok := w.paths[account.Address]
|
||||
if !ok {
|
||||
return nil, accounts.ErrUnknownAccount
|
||||
}
|
||||
// Ensure the wallet is capable of signing the given transaction
|
||||
if chainID != nil && w.version[0] <= 1 && w.version[1] <= 0 && w.version[2] <= 2 {
|
||||
return nil, fmt.Errorf("Ledger v%d.%d.%d doesn't support signing this transaction, please update to v1.0.3 at least", w.version[0], w.version[1], w.version[2])
|
||||
}
|
||||
// All infos gathered and metadata checks out, request signing
|
||||
<-w.commsLock
|
||||
defer func() { w.commsLock <- struct{}{} }()
|
||||
|
||||
return w.ledgerSign(path, account.Address, tx, chainID)
|
||||
}
|
||||
|
||||
// SignHashWithPassphrase implements accounts.Wallet, however signing arbitrary
|
||||
// data is not supported for Ledger wallets, so this method will always return
|
||||
// an error.
|
||||
func (w *ledgerWallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) {
|
||||
return nil, accounts.ErrNotSupported
|
||||
}
|
||||
|
||||
// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given
|
||||
// transaction with the given account using passphrase as extra authentication.
|
||||
// Since the Ledger does not support extra passphrases, it is silently ignored.
|
||||
func (w *ledgerWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||
return w.SignTx(account, tx, chainID)
|
||||
}
|
||||
|
||||
// ledgerVersion retrieves the current version of the Ethereum wallet app running
|
||||
// on the Ledger wallet.
|
||||
//
|
||||
// The version retrieval protocol is defined as follows:
|
||||
//
|
||||
// CLA | INS | P1 | P2 | Lc | Le
|
||||
// ----+-----+----+----+----+---
|
||||
// E0 | 06 | 00 | 00 | 00 | 04
|
||||
//
|
||||
// With no input data, and the output data being:
|
||||
//
|
||||
// Description | Length
|
||||
// ---------------------------------------------------+--------
|
||||
// Flags 01: arbitrary data signature enabled by user | 1 byte
|
||||
// Application major version | 1 byte
|
||||
// Application minor version | 1 byte
|
||||
// Application patch version | 1 byte
|
||||
func (w *ledgerWallet) ledgerVersion() ([3]byte, error) {
|
||||
// Send the request and wait for the response
|
||||
reply, err := w.ledgerExchange(ledgerOpGetConfiguration, 0, 0, nil)
|
||||
if err != nil {
|
||||
return [3]byte{}, err
|
||||
}
|
||||
if len(reply) != 4 {
|
||||
return [3]byte{}, errors.New("reply not of correct size")
|
||||
}
|
||||
// Cache the version for future reference
|
||||
var version [3]byte
|
||||
copy(version[:], reply[1:])
|
||||
return version, nil
|
||||
}
|
||||
|
||||
// ledgerDerive retrieves the currently active Ethereum address from a Ledger
|
||||
// wallet at the specified derivation path.
|
||||
//
|
||||
// The address derivation protocol is defined as follows:
|
||||
//
|
||||
// CLA | INS | P1 | P2 | Lc | Le
|
||||
// ----+-----+----+----+-----+---
|
||||
// E0 | 02 | 00 return address
|
||||
// 01 display address and confirm before returning
|
||||
// | 00: do not return the chain code
|
||||
// | 01: return the chain code
|
||||
// | var | 00
|
||||
//
|
||||
// Where the input data is:
|
||||
//
|
||||
// Description | Length
|
||||
// -------------------------------------------------+--------
|
||||
// Number of BIP 32 derivations to perform (max 10) | 1 byte
|
||||
// First derivation index (big endian) | 4 bytes
|
||||
// ... | 4 bytes
|
||||
// Last derivation index (big endian) | 4 bytes
|
||||
//
|
||||
// And the output data is:
|
||||
//
|
||||
// Description | Length
|
||||
// ------------------------+-------------------
|
||||
// Public Key length | 1 byte
|
||||
// Uncompressed Public Key | arbitrary
|
||||
// Ethereum address length | 1 byte
|
||||
// Ethereum address | 40 bytes hex ascii
|
||||
// Chain code if requested | 32 bytes
|
||||
func (w *ledgerWallet) ledgerDerive(derivationPath []uint32) (common.Address, error) {
|
||||
// Flatten the derivation path into the Ledger request
|
||||
path := make([]byte, 1+4*len(derivationPath))
|
||||
path[0] = byte(len(derivationPath))
|
||||
for i, component := range derivationPath {
|
||||
binary.BigEndian.PutUint32(path[1+4*i:], component)
|
||||
}
|
||||
// Send the request and wait for the response
|
||||
reply, err := w.ledgerExchange(ledgerOpRetrieveAddress, ledgerP1DirectlyFetchAddress, ledgerP2DiscardAddressChainCode, path)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
// Discard the public key, we don't need that for now
|
||||
if len(reply) < 1 || len(reply) < 1+int(reply[0]) {
|
||||
return common.Address{}, errors.New("reply lacks public key entry")
|
||||
}
|
||||
reply = reply[1+int(reply[0]):]
|
||||
|
||||
// Extract the Ethereum hex address string
|
||||
if len(reply) < 1 || len(reply) < 1+int(reply[0]) {
|
||||
return common.Address{}, errors.New("reply lacks address entry")
|
||||
}
|
||||
hexstr := reply[1 : 1+int(reply[0])]
|
||||
|
||||
// Decode the hex sting into an Ethereum address and return
|
||||
var address common.Address
|
||||
hex.Decode(address[:], hexstr)
|
||||
return address, nil
|
||||
}
|
||||
|
||||
// ledgerSign sends the transaction to the Ledger wallet, and waits for the user
|
||||
// to confirm or deny the transaction.
|
||||
//
|
||||
// The transaction signing protocol is defined as follows:
|
||||
//
|
||||
// CLA | INS | P1 | P2 | Lc | Le
|
||||
// ----+-----+----+----+-----+---
|
||||
// E0 | 04 | 00: first transaction data block
|
||||
// 80: subsequent transaction data block
|
||||
// | 00 | variable | variable
|
||||
//
|
||||
// Where the input for the first transaction block (first 255 bytes) is:
|
||||
//
|
||||
// Description | Length
|
||||
// -------------------------------------------------+----------
|
||||
// Number of BIP 32 derivations to perform (max 10) | 1 byte
|
||||
// First derivation index (big endian) | 4 bytes
|
||||
// ... | 4 bytes
|
||||
// Last derivation index (big endian) | 4 bytes
|
||||
// RLP transaction chunk | arbitrary
|
||||
//
|
||||
// And the input for subsequent transaction blocks (first 255 bytes) are:
|
||||
//
|
||||
// Description | Length
|
||||
// ----------------------+----------
|
||||
// RLP transaction chunk | arbitrary
|
||||
//
|
||||
// And the output data is:
|
||||
//
|
||||
// Description | Length
|
||||
// ------------+---------
|
||||
// signature V | 1 byte
|
||||
// signature R | 32 bytes
|
||||
// signature S | 32 bytes
|
||||
func (w *ledgerWallet) ledgerSign(derivationPath []uint32, address common.Address, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||
// We need to modify the timeouts to account for user feedback
|
||||
defer func(old time.Duration) { w.device.ReadTimeout = old }(w.device.ReadTimeout)
|
||||
w.device.ReadTimeout = time.Hour * 24 * 30 // Timeout requires a Ledger power cycle, only if you must
|
||||
|
||||
// Flatten the derivation path into the Ledger request
|
||||
path := make([]byte, 1+4*len(derivationPath))
|
||||
path[0] = byte(len(derivationPath))
|
||||
for i, component := range derivationPath {
|
||||
binary.BigEndian.PutUint32(path[1+4*i:], component)
|
||||
}
|
||||
// Create the transaction RLP based on whether legacy or EIP155 signing was requeste
|
||||
var (
|
||||
txrlp []byte
|
||||
err error
|
||||
)
|
||||
if chainID == nil {
|
||||
if txrlp, err = rlp.EncodeToBytes([]interface{}{tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data()}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if txrlp, err = rlp.EncodeToBytes([]interface{}{tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data(), chainID, big.NewInt(0), big.NewInt(0)}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
payload := append(path, txrlp...)
|
||||
|
||||
// Send the request and wait for the response
|
||||
var (
|
||||
op = ledgerP1InitTransactionData
|
||||
reply []byte
|
||||
)
|
||||
for len(payload) > 0 {
|
||||
// Calculate the size of the next data chunk
|
||||
chunk := 255
|
||||
if chunk > len(payload) {
|
||||
chunk = len(payload)
|
||||
}
|
||||
// Send the chunk over, ensuring it's processed correctly
|
||||
reply, err = w.ledgerExchange(ledgerOpSignTransaction, op, 0, payload[:chunk])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Shift the payload and ensure subsequent chunks are marked as such
|
||||
payload = payload[chunk:]
|
||||
op = ledgerP1ContTransactionData
|
||||
}
|
||||
// Extract the Ethereum signature and do a sanity validation
|
||||
if len(reply) != 65 {
|
||||
return nil, errors.New("reply lacks signature")
|
||||
}
|
||||
signature := append(reply[1:], reply[0])
|
||||
|
||||
// Create the correct signer and signature transform based on the chain ID
|
||||
var signer types.Signer
|
||||
if chainID == nil {
|
||||
signer = new(types.HomesteadSigner)
|
||||
} else {
|
||||
signer = types.NewEIP155Signer(chainID)
|
||||
signature[64] = signature[64] - byte(chainID.Uint64()*2+35)
|
||||
}
|
||||
// Inject the final signature into the transaction and sanity check the sender
|
||||
signed, err := tx.WithSignature(signer, signature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sender, err := types.Sender(signer, signed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sender != address {
|
||||
return nil, fmt.Errorf("signer mismatch: expected %s, got %s", address.Hex(), sender.Hex())
|
||||
}
|
||||
return signed, nil
|
||||
}
|
||||
|
||||
// ledgerExchange performs a data exchange with the Ledger wallet, sending it a
|
||||
// message and retrieving the response.
|
||||
//
|
||||
// The common transport header is defined as follows:
|
||||
//
|
||||
// Description | Length
|
||||
// --------------------------------------+----------
|
||||
// Communication channel ID (big endian) | 2 bytes
|
||||
// Command tag | 1 byte
|
||||
// Packet sequence index (big endian) | 2 bytes
|
||||
// Payload | arbitrary
|
||||
//
|
||||
// The Communication channel ID allows commands multiplexing over the same
|
||||
// physical link. It is not used for the time being, and should be set to 0101
|
||||
// to avoid compatibility issues with implementations ignoring a leading 00 byte.
|
||||
//
|
||||
// The Command tag describes the message content. Use TAG_APDU (0x05) for standard
|
||||
// APDU payloads, or TAG_PING (0x02) for a simple link test.
|
||||
//
|
||||
// The Packet sequence index describes the current sequence for fragmented payloads.
|
||||
// The first fragment index is 0x00.
|
||||
//
|
||||
// APDU Command payloads are encoded as follows:
|
||||
//
|
||||
// Description | Length
|
||||
// -----------------------------------
|
||||
// APDU length (big endian) | 2 bytes
|
||||
// APDU CLA | 1 byte
|
||||
// APDU INS | 1 byte
|
||||
// APDU P1 | 1 byte
|
||||
// APDU P2 | 1 byte
|
||||
// APDU length | 1 byte
|
||||
// Optional APDU data | arbitrary
|
||||
func (w *ledgerWallet) ledgerExchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) {
|
||||
// Construct the message payload, possibly split into multiple chunks
|
||||
apdu := make([]byte, 2, 7+len(data))
|
||||
|
||||
binary.BigEndian.PutUint16(apdu, uint16(5+len(data)))
|
||||
apdu = append(apdu, []byte{0xe0, byte(opcode), byte(p1), byte(p2), byte(len(data))}...)
|
||||
apdu = append(apdu, data...)
|
||||
|
||||
// Stream all the chunks to the device
|
||||
header := []byte{0x01, 0x01, 0x05, 0x00, 0x00} // Channel ID and command tag appended
|
||||
chunk := make([]byte, 64)
|
||||
space := len(chunk) - len(header)
|
||||
|
||||
for i := 0; len(apdu) > 0; i++ {
|
||||
// Construct the new message to stream
|
||||
chunk = append(chunk[:0], header...)
|
||||
binary.BigEndian.PutUint16(chunk[3:], uint16(i))
|
||||
|
||||
if len(apdu) > space {
|
||||
chunk = append(chunk, apdu[:space]...)
|
||||
apdu = apdu[space:]
|
||||
} else {
|
||||
chunk = append(chunk, apdu...)
|
||||
apdu = nil
|
||||
}
|
||||
// Send over to the device
|
||||
if glog.V(logger.Detail) {
|
||||
glog.Infof("-> %03d.%03d: %x", w.device.Bus, w.device.Address, chunk)
|
||||
}
|
||||
if _, err := w.input.Write(chunk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Stream the reply back from the wallet in 64 byte chunks
|
||||
var reply []byte
|
||||
chunk = chunk[:64] // Yeah, we surely have enough space
|
||||
for {
|
||||
// Read the next chunk from the Ledger wallet
|
||||
if _, err := io.ReadFull(w.output, chunk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if glog.V(logger.Detail) {
|
||||
glog.Infof("<- %03d.%03d: %x", w.device.Bus, w.device.Address, chunk)
|
||||
}
|
||||
// Make sure the transport header matches
|
||||
if chunk[0] != 0x01 || chunk[1] != 0x01 || chunk[2] != 0x05 {
|
||||
return nil, errReplyInvalidHeader
|
||||
}
|
||||
// If it's the first chunk, retrieve the total message length
|
||||
var payload []byte
|
||||
|
||||
if chunk[3] == 0x00 && chunk[4] == 0x00 {
|
||||
reply = make([]byte, 0, int(binary.BigEndian.Uint16(chunk[5:7])))
|
||||
payload = chunk[7:]
|
||||
} else {
|
||||
payload = chunk[5:]
|
||||
}
|
||||
// Append to the reply and stop when filled up
|
||||
if left := cap(reply) - len(reply); left > len(payload) {
|
||||
reply = append(reply, payload...)
|
||||
} else {
|
||||
reply = append(reply, payload[:left]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
return reply[:len(reply)-2], nil
|
||||
}
|
||||
29
accounts/usbwallet/usbwallet.go
Normal file
29
accounts/usbwallet/usbwallet.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !ios
|
||||
|
||||
// Package usbwallet implements support for USB hardware wallets.
|
||||
package usbwallet
|
||||
|
||||
import "github.com/karalabe/gousb/usb"
|
||||
|
||||
// deviceID is a combined vendor/product identifier to uniquely identify a USB
|
||||
// hardware device.
|
||||
type deviceID struct {
|
||||
Vendor usb.ID // The Vendor identifer
|
||||
Product usb.ID // The Product identifier
|
||||
}
|
||||
38
accounts/usbwallet/usbwallet_ios.go
Normal file
38
accounts/usbwallet/usbwallet_ios.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// This file contains the implementation for interacting with the Ledger hardware
|
||||
// wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo:
|
||||
// https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc
|
||||
|
||||
// +build ios
|
||||
|
||||
package usbwallet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
)
|
||||
|
||||
// Here be dragons! There is no USB support on iOS.
|
||||
|
||||
// ErrIOSNotSupported is returned for all USB hardware backends on iOS.
|
||||
var ErrIOSNotSupported = errors.New("no USB support on iOS")
|
||||
|
||||
func NewLedgerHub() (accounts.Backend, error) {
|
||||
return nil, ErrIOSNotSupported
|
||||
}
|
||||
@@ -236,6 +236,14 @@ func goToolArch(arch string, subcmd string, args ...string) *exec.Cmd {
|
||||
gocmd := filepath.Join(runtime.GOROOT(), "bin", "go")
|
||||
cmd := exec.Command(gocmd, subcmd)
|
||||
cmd.Args = append(cmd.Args, args...)
|
||||
|
||||
if subcmd == "build" || subcmd == "install" || subcmd == "test" {
|
||||
// Go CGO has a Windows linker error prior to 1.8 (https://github.com/golang/go/issues/8756).
|
||||
// Work around issue by allowing multiple definitions for <1.8 builds.
|
||||
if runtime.GOOS == "windows" && runtime.Version() < "go1.8" {
|
||||
cmd.Args = append(cmd.Args, []string{"-ldflags", "-extldflags -Wl,--allow-multiple-definition"}...)
|
||||
}
|
||||
}
|
||||
cmd.Env = []string{
|
||||
"GO15VENDOREXPERIMENT=1",
|
||||
"GOPATH=" + build.GOPATH(),
|
||||
|
||||
@@ -94,7 +94,9 @@ func main() {
|
||||
abi, _ := json.Marshal(contract.Info.AbiDefinition) // Flatten the compiler parse
|
||||
abis = append(abis, string(abi))
|
||||
bins = append(bins, contract.Code)
|
||||
types = append(types, name)
|
||||
|
||||
nameParts := strings.Split(name, ":")
|
||||
types = append(types, nameParts[len(nameParts)-1])
|
||||
}
|
||||
} else {
|
||||
// Otherwise load up the ABI, optional bytecode and type name from the parameters
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
@@ -180,31 +181,36 @@ nodes.
|
||||
|
||||
func accountList(ctx *cli.Context) error {
|
||||
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
|
||||
for i, acct := range stack.AccountManager().Accounts() {
|
||||
fmt.Printf("Account #%d: {%x} %s\n", i, acct.Address, acct.File)
|
||||
|
||||
var index int
|
||||
for _, wallet := range stack.AccountManager().Wallets() {
|
||||
for _, account := range wallet.Accounts() {
|
||||
fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, &account.URL)
|
||||
index++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tries unlocking the specified account a few times.
|
||||
func unlockAccount(ctx *cli.Context, accman *accounts.Manager, address string, i int, passwords []string) (accounts.Account, string) {
|
||||
account, err := utils.MakeAddress(accman, address)
|
||||
func unlockAccount(ctx *cli.Context, ks *keystore.KeyStore, address string, i int, passwords []string) (accounts.Account, string) {
|
||||
account, err := utils.MakeAddress(ks, address)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not list accounts: %v", err)
|
||||
}
|
||||
for trials := 0; trials < 3; trials++ {
|
||||
prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3)
|
||||
password := getPassPhrase(prompt, false, i, passwords)
|
||||
err = accman.Unlock(account, password)
|
||||
err = ks.Unlock(account, password)
|
||||
if err == nil {
|
||||
glog.V(logger.Info).Infof("Unlocked account %x", account.Address)
|
||||
return account, password
|
||||
}
|
||||
if err, ok := err.(*accounts.AmbiguousAddrError); ok {
|
||||
if err, ok := err.(*keystore.AmbiguousAddrError); ok {
|
||||
glog.V(logger.Info).Infof("Unlocked account %x", account.Address)
|
||||
return ambiguousAddrRecovery(accman, err, password), password
|
||||
return ambiguousAddrRecovery(ks, err, password), password
|
||||
}
|
||||
if err != accounts.ErrDecrypt {
|
||||
if err != keystore.ErrDecrypt {
|
||||
// No need to prompt again if the error is not decryption-related.
|
||||
break
|
||||
}
|
||||
@@ -244,15 +250,15 @@ func getPassPhrase(prompt string, confirmation bool, i int, passwords []string)
|
||||
return password
|
||||
}
|
||||
|
||||
func ambiguousAddrRecovery(am *accounts.Manager, err *accounts.AmbiguousAddrError, auth string) accounts.Account {
|
||||
func ambiguousAddrRecovery(ks *keystore.KeyStore, err *keystore.AmbiguousAddrError, auth string) accounts.Account {
|
||||
fmt.Printf("Multiple key files exist for address %x:\n", err.Addr)
|
||||
for _, a := range err.Matches {
|
||||
fmt.Println(" ", a.File)
|
||||
fmt.Println(" ", a.URL)
|
||||
}
|
||||
fmt.Println("Testing your passphrase against all of them...")
|
||||
var match *accounts.Account
|
||||
for _, a := range err.Matches {
|
||||
if err := am.Unlock(a, auth); err == nil {
|
||||
if err := ks.Unlock(a, auth); err == nil {
|
||||
match = &a
|
||||
break
|
||||
}
|
||||
@@ -260,11 +266,11 @@ func ambiguousAddrRecovery(am *accounts.Manager, err *accounts.AmbiguousAddrErro
|
||||
if match == nil {
|
||||
utils.Fatalf("None of the listed files could be unlocked.")
|
||||
}
|
||||
fmt.Printf("Your passphrase unlocked %s\n", match.File)
|
||||
fmt.Printf("Your passphrase unlocked %s\n", match.URL)
|
||||
fmt.Println("In order to avoid this warning, you need to remove the following duplicate key files:")
|
||||
for _, a := range err.Matches {
|
||||
if a != *match {
|
||||
fmt.Println(" ", a.File)
|
||||
fmt.Println(" ", a.URL)
|
||||
}
|
||||
}
|
||||
return *match
|
||||
@@ -275,7 +281,8 @@ func accountCreate(ctx *cli.Context) error {
|
||||
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
|
||||
password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
|
||||
|
||||
account, err := stack.AccountManager().NewAccount(password)
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
account, err := ks.NewAccount(password)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
@@ -290,9 +297,11 @@ func accountUpdate(ctx *cli.Context) error {
|
||||
utils.Fatalf("No accounts specified to update")
|
||||
}
|
||||
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
|
||||
account, oldPassword := unlockAccount(ctx, stack.AccountManager(), ctx.Args().First(), 0, nil)
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
|
||||
account, oldPassword := unlockAccount(ctx, ks, ctx.Args().First(), 0, nil)
|
||||
newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil)
|
||||
if err := stack.AccountManager().Update(account, oldPassword, newPassword); err != nil {
|
||||
if err := ks.Update(account, oldPassword, newPassword); err != nil {
|
||||
utils.Fatalf("Could not update the account: %v", err)
|
||||
}
|
||||
return nil
|
||||
@@ -310,7 +319,9 @@ func importWallet(ctx *cli.Context) error {
|
||||
|
||||
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
|
||||
passphrase := getPassPhrase("", false, 0, utils.MakePasswordList(ctx))
|
||||
acct, err := stack.AccountManager().ImportPreSaleKey(keyJson, passphrase)
|
||||
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
acct, err := ks.ImportPreSaleKey(keyJson, passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
@@ -329,7 +340,9 @@ func accountImport(ctx *cli.Context) error {
|
||||
}
|
||||
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
|
||||
passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
|
||||
acct, err := stack.AccountManager().ImportECDSA(key, passphrase)
|
||||
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
acct, err := ks.ImportECDSA(key, passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not create the account: %v", err)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import (
|
||||
func tmpDatadirWithKeystore(t *testing.T) string {
|
||||
datadir := tmpdir(t)
|
||||
keystore := filepath.Join(datadir, "keystore")
|
||||
source := filepath.Join("..", "..", "accounts", "testdata", "keystore")
|
||||
source := filepath.Join("..", "..", "accounts", "keystore", "testdata", "keystore")
|
||||
if err := cp.CopyAll(keystore, source); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -53,15 +53,15 @@ func TestAccountList(t *testing.T) {
|
||||
defer geth.expectExit()
|
||||
if runtime.GOOS == "windows" {
|
||||
geth.expect(`
|
||||
Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} {{.Datadir}}\keystore\UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
|
||||
Account #1: {f466859ead1932d743d622cb74fc058882e8648a} {{.Datadir}}\keystore\aaa
|
||||
Account #2: {289d485d9771714cce91d3393d764e1311907acc} {{.Datadir}}\keystore\zzz
|
||||
Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} keystore://{{.Datadir}}\keystore\UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
|
||||
Account #1: {f466859ead1932d743d622cb74fc058882e8648a} keystore://{{.Datadir}}\keystore\aaa
|
||||
Account #2: {289d485d9771714cce91d3393d764e1311907acc} keystore://{{.Datadir}}\keystore\zzz
|
||||
`)
|
||||
} else {
|
||||
geth.expect(`
|
||||
Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} {{.Datadir}}/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
|
||||
Account #1: {f466859ead1932d743d622cb74fc058882e8648a} {{.Datadir}}/keystore/aaa
|
||||
Account #2: {289d485d9771714cce91d3393d764e1311907acc} {{.Datadir}}/keystore/zzz
|
||||
Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} keystore://{{.Datadir}}/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
|
||||
Account #1: {f466859ead1932d743d622cb74fc058882e8648a} keystore://{{.Datadir}}/keystore/aaa
|
||||
Account #2: {289d485d9771714cce91d3393d764e1311907acc} keystore://{{.Datadir}}/keystore/zzz
|
||||
`)
|
||||
}
|
||||
}
|
||||
@@ -230,7 +230,7 @@ Fatal: Failed to unlock account 0 (could not decrypt key with given passphrase)
|
||||
}
|
||||
|
||||
func TestUnlockFlagAmbiguous(t *testing.T) {
|
||||
store := filepath.Join("..", "..", "accounts", "testdata", "dupes")
|
||||
store := filepath.Join("..", "..", "accounts", "keystore", "testdata", "dupes")
|
||||
geth := runGeth(t,
|
||||
"--keystore", store, "--nat", "none", "--nodiscover", "--dev",
|
||||
"--unlock", "f466859ead1932d743d622cb74fc058882e8648a",
|
||||
@@ -247,12 +247,12 @@ Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
|
||||
!! Unsupported terminal, password will be echoed.
|
||||
Passphrase: {{.InputLine "foobar"}}
|
||||
Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a:
|
||||
{{keypath "1"}}
|
||||
{{keypath "2"}}
|
||||
keystore://{{keypath "1"}}
|
||||
keystore://{{keypath "2"}}
|
||||
Testing your passphrase against all of them...
|
||||
Your passphrase unlocked {{keypath "1"}}
|
||||
Your passphrase unlocked keystore://{{keypath "1"}}
|
||||
In order to avoid this warning, you need to remove the following duplicate key files:
|
||||
{{keypath "2"}}
|
||||
keystore://{{keypath "2"}}
|
||||
`)
|
||||
geth.expectExit()
|
||||
|
||||
@@ -267,7 +267,7 @@ In order to avoid this warning, you need to remove the following duplicate key f
|
||||
}
|
||||
|
||||
func TestUnlockFlagAmbiguousWrongPassword(t *testing.T) {
|
||||
store := filepath.Join("..", "..", "accounts", "testdata", "dupes")
|
||||
store := filepath.Join("..", "..", "accounts", "keystore", "testdata", "dupes")
|
||||
geth := runGeth(t,
|
||||
"--keystore", store, "--nat", "none", "--nodiscover", "--dev",
|
||||
"--unlock", "f466859ead1932d743d622cb74fc058882e8648a")
|
||||
@@ -283,8 +283,8 @@ Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
|
||||
!! Unsupported terminal, password will be echoed.
|
||||
Passphrase: {{.InputLine "wrong"}}
|
||||
Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a:
|
||||
{{keypath "1"}}
|
||||
{{keypath "2"}}
|
||||
keystore://{{keypath "1"}}
|
||||
keystore://{{keypath "2"}}
|
||||
Testing your passphrase against all of them...
|
||||
Fatal: None of the listed files could be unlocked.
|
||||
`)
|
||||
|
||||
@@ -25,11 +25,14 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
"github.com/ethereum/go-ethereum/contracts/release"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/internal/debug"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
@@ -245,14 +248,50 @@ func startNode(ctx *cli.Context, stack *node.Node) {
|
||||
utils.StartNode(stack)
|
||||
|
||||
// Unlock any account specifically requested
|
||||
accman := stack.AccountManager()
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
|
||||
passwords := utils.MakePasswordList(ctx)
|
||||
accounts := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
|
||||
for i, account := range accounts {
|
||||
unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
|
||||
for i, account := range unlocks {
|
||||
if trimmed := strings.TrimSpace(account); trimmed != "" {
|
||||
unlockAccount(ctx, accman, trimmed, i, passwords)
|
||||
unlockAccount(ctx, ks, trimmed, i, passwords)
|
||||
}
|
||||
}
|
||||
// Register wallet event handlers to open and auto-derive wallets
|
||||
events := make(chan accounts.WalletEvent, 16)
|
||||
stack.AccountManager().Subscribe(events)
|
||||
|
||||
go func() {
|
||||
// Create an chain state reader for self-derivation
|
||||
rpcClient, err := stack.Attach()
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to attach to self: %v", err)
|
||||
}
|
||||
stateReader := ethclient.NewClient(rpcClient)
|
||||
|
||||
// Open and self derive any wallets already attached
|
||||
for _, wallet := range stack.AccountManager().Wallets() {
|
||||
if err := wallet.Open(""); err != nil {
|
||||
glog.V(logger.Warn).Infof("Failed to open wallet %s: %v", wallet.URL(), err)
|
||||
} else {
|
||||
wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
|
||||
}
|
||||
}
|
||||
// Listen for wallet event till termination
|
||||
for event := range events {
|
||||
if event.Arrive {
|
||||
if err := event.Wallet.Open(""); err != nil {
|
||||
glog.V(logger.Info).Infof("New wallet appeared: %s, failed to open: %s", event.Wallet.URL(), err)
|
||||
} else {
|
||||
glog.V(logger.Info).Infof("New wallet appeared: %s, %s", event.Wallet.URL(), event.Wallet.Status())
|
||||
event.Wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
|
||||
}
|
||||
} else {
|
||||
glog.V(logger.Info).Infof("Old wallet dropped: %s", event.Wallet.URL())
|
||||
event.Wallet.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Start auxiliary services if enabled
|
||||
if ctx.GlobalBool(utils.MiningEnabledFlag.Name) {
|
||||
var ethereum *eth.Ethereum
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
@@ -99,17 +100,18 @@ func MakeSystemNode(privkey string, test *tests.BlockTest) (*node.Node, error) {
|
||||
return nil, err
|
||||
}
|
||||
// Create the keystore and inject an unlocked account if requested
|
||||
accman := stack.AccountManager()
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
|
||||
if len(privkey) > 0 {
|
||||
key, err := crypto.HexToECDSA(privkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a, err := accman.ImportECDSA(key, "")
|
||||
a, err := ks.ImportECDSA(key, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := accman.Unlock(a, ""); err != nil {
|
||||
if err := ks.Unlock(a, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
39
cmd/swarm/cleandb.go
Normal file
39
cmd/swarm/cleandb.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/ethereum/go-ethereum/swarm/storage"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
func cleandb(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) != 1 {
|
||||
log.Fatal("need path to chunks database as the first and only argument")
|
||||
}
|
||||
|
||||
chunkDbPath := args[0]
|
||||
hash := storage.MakeHashFunc("SHA3")
|
||||
dbStore, err := storage.NewDbStore(chunkDbPath, hash, 10000000, 0)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot initialise dbstore: %v", err)
|
||||
}
|
||||
dbStore.Cleanup()
|
||||
}
|
||||
@@ -21,11 +21,14 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
@@ -152,6 +155,52 @@ The output of this command is supposed to be machine-readable.
|
||||
ArgsUsage: " <file>",
|
||||
Description: `
|
||||
Prints the swarm hash of file or directory.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "manifest",
|
||||
Usage: "update a MANIFEST",
|
||||
ArgsUsage: "manifest COMMAND",
|
||||
Description: `
|
||||
Updates a MANIFEST by adding/removing/updating the hash of a path.
|
||||
`,
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Action: add,
|
||||
Name: "add",
|
||||
Usage: "add a new path to the manifest",
|
||||
ArgsUsage: "<MANIFEST> <path> <hash> [<content-type>]",
|
||||
Description: `
|
||||
Adds a new path to the manifest
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: update,
|
||||
Name: "update",
|
||||
Usage: "update the hash for an already existing path in the manifest",
|
||||
ArgsUsage: "<MANIFEST> <path> <newhash> [<newcontent-type>]",
|
||||
Description: `
|
||||
Update the hash for an already existing path in the manifest
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: remove,
|
||||
Name: "remove",
|
||||
Usage: "removes a path from the manifest",
|
||||
ArgsUsage: "<MANIFEST> <path>",
|
||||
Description: `
|
||||
Removes a path from the manifest
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: cleandb,
|
||||
Name: "cleandb",
|
||||
Usage: "Cleans database of corrupted entries",
|
||||
ArgsUsage: " ",
|
||||
Description: `
|
||||
Cleans database of corrupted entries.
|
||||
`,
|
||||
},
|
||||
}
|
||||
@@ -224,6 +273,14 @@ func bzzd(ctx *cli.Context) error {
|
||||
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
|
||||
registerBzzService(ctx, stack)
|
||||
utils.StartNode(stack)
|
||||
go func() {
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc, syscall.SIGTERM)
|
||||
defer signal.Stop(sigc)
|
||||
<-sigc
|
||||
glog.V(logger.Info).Infoln("Got sigterm, shutting down...")
|
||||
stack.Stop()
|
||||
}()
|
||||
networkId := ctx.GlobalUint64(SwarmNetworkIdFlag.Name)
|
||||
// Add bootnodes as initial peers.
|
||||
if ctx.GlobalIsSet(utils.BootnodesFlag.Name) {
|
||||
@@ -290,29 +347,36 @@ func getAccount(ctx *cli.Context, stack *node.Node) *ecdsa.PrivateKey {
|
||||
return key
|
||||
}
|
||||
// Otherwise try getting it from the keystore.
|
||||
return decryptStoreAccount(stack.AccountManager(), keyid)
|
||||
am := stack.AccountManager()
|
||||
ks := am.Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
|
||||
return decryptStoreAccount(ks, keyid)
|
||||
}
|
||||
|
||||
func decryptStoreAccount(accman *accounts.Manager, account string) *ecdsa.PrivateKey {
|
||||
func decryptStoreAccount(ks *keystore.KeyStore, account string) *ecdsa.PrivateKey {
|
||||
var a accounts.Account
|
||||
var err error
|
||||
if common.IsHexAddress(account) {
|
||||
a, err = accman.Find(accounts.Account{Address: common.HexToAddress(account)})
|
||||
} else if ix, ixerr := strconv.Atoi(account); ixerr == nil {
|
||||
a, err = accman.AccountByIndex(ix)
|
||||
a, err = ks.Find(accounts.Account{Address: common.HexToAddress(account)})
|
||||
} else if ix, ixerr := strconv.Atoi(account); ixerr == nil && ix > 0 {
|
||||
if accounts := ks.Accounts(); len(accounts) > ix {
|
||||
a = accounts[ix]
|
||||
} else {
|
||||
err = fmt.Errorf("index %d higher than number of accounts %d", ix, len(accounts))
|
||||
}
|
||||
} else {
|
||||
utils.Fatalf("Can't find swarm account key %s", account)
|
||||
}
|
||||
if err != nil {
|
||||
utils.Fatalf("Can't find swarm account key: %v", err)
|
||||
}
|
||||
keyjson, err := ioutil.ReadFile(a.File)
|
||||
keyjson, err := ioutil.ReadFile(a.URL.Path)
|
||||
if err != nil {
|
||||
utils.Fatalf("Can't load swarm account key: %v", err)
|
||||
}
|
||||
for i := 1; i <= 3; i++ {
|
||||
passphrase := promptPassphrase(fmt.Sprintf("Unlocking swarm account %s [%d/3]", a.Address.Hex(), i))
|
||||
key, err := accounts.DecryptKey(keyjson, passphrase)
|
||||
key, err := keystore.DecryptKey(keyjson, passphrase)
|
||||
if err == nil {
|
||||
return key.PrivateKey
|
||||
}
|
||||
|
||||
360
cmd/swarm/manifest.go
Normal file
360
cmd/swarm/manifest.go
Normal file
@@ -0,0 +1,360 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Command MANIFEST update
|
||||
package main
|
||||
|
||||
import (
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
"log"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
func add(ctx *cli.Context) {
|
||||
|
||||
args := ctx.Args()
|
||||
if len(args) < 3 {
|
||||
log.Fatal("need atleast three arguments <MHASH> <path> <HASH> [<content-type>]")
|
||||
}
|
||||
|
||||
var (
|
||||
mhash = args[0]
|
||||
path = args[1]
|
||||
hash = args[2]
|
||||
|
||||
ctype string
|
||||
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
|
||||
mroot manifest
|
||||
)
|
||||
|
||||
|
||||
if len(args) > 3 {
|
||||
ctype = args[3]
|
||||
} else {
|
||||
ctype = mime.TypeByExtension(filepath.Ext(path))
|
||||
}
|
||||
|
||||
newManifest := addEntryToManifest (ctx, mhash, path, hash, ctype)
|
||||
fmt.Println(newManifest)
|
||||
|
||||
if !wantManifest {
|
||||
// Print the manifest. This is the only output to stdout.
|
||||
mrootJSON, _ := json.MarshalIndent(mroot, "", " ")
|
||||
fmt.Println(string(mrootJSON))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func update(ctx *cli.Context) {
|
||||
|
||||
args := ctx.Args()
|
||||
if len(args) < 3 {
|
||||
log.Fatal("need atleast three arguments <MHASH> <path> <HASH>")
|
||||
}
|
||||
|
||||
var (
|
||||
mhash = args[0]
|
||||
path = args[1]
|
||||
hash = args[2]
|
||||
|
||||
ctype string
|
||||
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
|
||||
mroot manifest
|
||||
)
|
||||
if len(args) > 3 {
|
||||
ctype = args[3]
|
||||
} else {
|
||||
ctype = mime.TypeByExtension(filepath.Ext(path))
|
||||
}
|
||||
|
||||
newManifest := updateEntryInManifest (ctx, mhash, path, hash, ctype)
|
||||
fmt.Println(newManifest)
|
||||
|
||||
if !wantManifest {
|
||||
// Print the manifest. This is the only output to stdout.
|
||||
mrootJSON, _ := json.MarshalIndent(mroot, "", " ")
|
||||
fmt.Println(string(mrootJSON))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func remove(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) < 2 {
|
||||
log.Fatal("need atleast two arguments <MHASH> <path>")
|
||||
}
|
||||
|
||||
var (
|
||||
mhash = args[0]
|
||||
path = args[1]
|
||||
|
||||
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
|
||||
mroot manifest
|
||||
)
|
||||
|
||||
newManifest := removeEntryFromManifest (ctx, mhash, path)
|
||||
fmt.Println(newManifest)
|
||||
|
||||
if !wantManifest {
|
||||
// Print the manifest. This is the only output to stdout.
|
||||
mrootJSON, _ := json.MarshalIndent(mroot, "", " ")
|
||||
fmt.Println(string(mrootJSON))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func addEntryToManifest(ctx *cli.Context, mhash , path, hash , ctype string) string {
|
||||
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client = &client{api: bzzapi}
|
||||
longestPathEntry = manifestEntry{
|
||||
Path: "",
|
||||
Hash: "",
|
||||
ContentType: "",
|
||||
}
|
||||
)
|
||||
|
||||
mroot, err := client.downloadManifest(mhash)
|
||||
if err != nil {
|
||||
log.Fatalln("manifest download failed:", err)
|
||||
}
|
||||
|
||||
//TODO: check if the "hash" to add is valid and present in swarm
|
||||
_, err = client.downloadManifest(hash)
|
||||
if err != nil {
|
||||
log.Fatalln("hash to add is not present:", err)
|
||||
}
|
||||
|
||||
|
||||
// See if we path is in this Manifest or do we have to dig deeper
|
||||
for _, entry := range mroot.Entries {
|
||||
if path == entry.Path {
|
||||
log.Fatal(path, "Already present, not adding anything")
|
||||
}else {
|
||||
if entry.ContentType == "application/bzz-manifest+json" {
|
||||
prfxlen := strings.HasPrefix(path, entry.Path)
|
||||
if prfxlen && len(path) > len(longestPathEntry.Path) {
|
||||
longestPathEntry = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if longestPathEntry.Path != "" {
|
||||
// Load the child Manifest add the entry there
|
||||
newPath := path[len(longestPathEntry.Path):]
|
||||
newHash := addEntryToManifest (ctx, longestPathEntry.Hash, newPath, hash, ctype)
|
||||
|
||||
// Replace the hash for parent Manifests
|
||||
newMRoot := manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if longestPathEntry.Path == entry.Path {
|
||||
entry.Hash = newHash
|
||||
}
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
}
|
||||
mroot = newMRoot
|
||||
} else {
|
||||
// Add the entry in the leaf Manifest
|
||||
newEntry := manifestEntry{
|
||||
Path: path,
|
||||
Hash: hash,
|
||||
ContentType: ctype,
|
||||
}
|
||||
mroot.Entries = append(mroot.Entries, newEntry)
|
||||
}
|
||||
|
||||
|
||||
newManifestHash, err := client.uploadManifest(mroot)
|
||||
if err != nil {
|
||||
log.Fatalln("manifest upload failed:", err)
|
||||
}
|
||||
return newManifestHash
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
func updateEntryInManifest(ctx *cli.Context, mhash , path, hash , ctype string) string {
|
||||
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client = &client{api: bzzapi}
|
||||
newEntry = manifestEntry{
|
||||
Path: "",
|
||||
Hash: "",
|
||||
ContentType: "",
|
||||
}
|
||||
longestPathEntry = manifestEntry{
|
||||
Path: "",
|
||||
Hash: "",
|
||||
ContentType: "",
|
||||
}
|
||||
)
|
||||
|
||||
mroot, err := client.downloadManifest(mhash)
|
||||
if err != nil {
|
||||
log.Fatalln("manifest download failed:", err)
|
||||
}
|
||||
|
||||
//TODO: check if the "hash" with which to update is valid and present in swarm
|
||||
|
||||
|
||||
// See if we path is in this Manifest or do we have to dig deeper
|
||||
for _, entry := range mroot.Entries {
|
||||
if path == entry.Path {
|
||||
newEntry = entry
|
||||
}else {
|
||||
if entry.ContentType == "application/bzz-manifest+json" {
|
||||
prfxlen := strings.HasPrefix(path, entry.Path)
|
||||
if prfxlen && len(path) > len(longestPathEntry.Path) {
|
||||
longestPathEntry = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if longestPathEntry.Path == "" && newEntry.Path == "" {
|
||||
log.Fatal(path, " Path not present in the Manifest, not setting anything")
|
||||
}
|
||||
|
||||
if longestPathEntry.Path != "" {
|
||||
// Load the child Manifest add the entry there
|
||||
newPath := path[len(longestPathEntry.Path):]
|
||||
newHash := updateEntryInManifest (ctx, longestPathEntry.Hash, newPath, hash, ctype)
|
||||
|
||||
// Replace the hash for parent Manifests
|
||||
newMRoot := manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if longestPathEntry.Path == entry.Path {
|
||||
entry.Hash = newHash
|
||||
}
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
|
||||
}
|
||||
mroot = newMRoot
|
||||
}
|
||||
|
||||
if newEntry.Path != "" {
|
||||
// Replace the hash for leaf Manifest
|
||||
newMRoot := manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if newEntry.Path == entry.Path {
|
||||
myEntry := manifestEntry{
|
||||
Path: entry.Path,
|
||||
Hash: hash,
|
||||
ContentType: ctype,
|
||||
}
|
||||
newMRoot.Entries = append(newMRoot.Entries, myEntry)
|
||||
} else {
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
}
|
||||
}
|
||||
mroot = newMRoot
|
||||
}
|
||||
|
||||
|
||||
newManifestHash, err := client.uploadManifest(mroot)
|
||||
if err != nil {
|
||||
log.Fatalln("manifest upload failed:", err)
|
||||
}
|
||||
return newManifestHash
|
||||
}
|
||||
|
||||
func removeEntryFromManifest(ctx *cli.Context, mhash , path string) string {
|
||||
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client = &client{api: bzzapi}
|
||||
entryToRemove = manifestEntry{
|
||||
Path: "",
|
||||
Hash: "",
|
||||
ContentType: "",
|
||||
}
|
||||
longestPathEntry = manifestEntry{
|
||||
Path: "",
|
||||
Hash: "",
|
||||
ContentType: "",
|
||||
}
|
||||
)
|
||||
|
||||
mroot, err := client.downloadManifest(mhash)
|
||||
if err != nil {
|
||||
log.Fatalln("manifest download failed:", err)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// See if we path is in this Manifest or do we have to dig deeper
|
||||
for _, entry := range mroot.Entries {
|
||||
if path == entry.Path {
|
||||
entryToRemove = entry
|
||||
}else {
|
||||
if entry.ContentType == "application/bzz-manifest+json" {
|
||||
prfxlen := strings.HasPrefix(path, entry.Path)
|
||||
if prfxlen && len(path) > len(longestPathEntry.Path) {
|
||||
longestPathEntry = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if longestPathEntry.Path == "" && entryToRemove.Path == "" {
|
||||
log.Fatal(path, "Path not present in the Manifest, not removing anything")
|
||||
}
|
||||
|
||||
if longestPathEntry.Path != "" {
|
||||
// Load the child Manifest remove the entry there
|
||||
newPath := path[len(longestPathEntry.Path):]
|
||||
newHash := removeEntryFromManifest (ctx, longestPathEntry.Hash, newPath)
|
||||
|
||||
// Replace the hash for parent Manifests
|
||||
newMRoot := manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if longestPathEntry.Path == entry.Path {
|
||||
entry.Hash = newHash
|
||||
}
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
}
|
||||
mroot = newMRoot
|
||||
}
|
||||
|
||||
if entryToRemove.Path != "" {
|
||||
// remove the entry in this Manifest
|
||||
newMRoot := manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if entryToRemove.Path != entry.Path {
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
}
|
||||
}
|
||||
mroot = newMRoot
|
||||
}
|
||||
|
||||
|
||||
newManifestHash, err := client.uploadManifest(mroot)
|
||||
if err != nil {
|
||||
log.Fatalln("manifest upload failed:", err)
|
||||
}
|
||||
return newManifestHash
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -229,3 +229,29 @@ func (c *client) postRaw(mimetype string, size int64, body io.ReadCloser) (strin
|
||||
content, err := ioutil.ReadAll(resp.Body)
|
||||
return string(content), err
|
||||
}
|
||||
|
||||
func (c *client) downloadManifest(mhash string) (manifest, error) {
|
||||
|
||||
mroot := manifest{}
|
||||
req, err := http.NewRequest("GET", c.api + "/bzzr:/" + mhash, nil)
|
||||
if err != nil {
|
||||
return mroot, err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return mroot, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return mroot, fmt.Errorf("bad status: %s", resp.Status)
|
||||
|
||||
}
|
||||
content, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
err = json.Unmarshal(content, &mroot)
|
||||
if err != nil {
|
||||
return mroot, fmt.Errorf("Manifest %v is malformed: %v", mhash, err)
|
||||
}
|
||||
return mroot, err
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
|
||||
"github.com/ethereum/ethash"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
@@ -587,23 +588,27 @@ func MakeDatabaseHandles() int {
|
||||
|
||||
// MakeAddress converts an account specified directly as a hex encoded string or
|
||||
// a key index in the key store to an internal account representation.
|
||||
func MakeAddress(accman *accounts.Manager, account string) (accounts.Account, error) {
|
||||
func MakeAddress(ks *keystore.KeyStore, account string) (accounts.Account, error) {
|
||||
// If the specified account is a valid address, return it
|
||||
if common.IsHexAddress(account) {
|
||||
return accounts.Account{Address: common.HexToAddress(account)}, nil
|
||||
}
|
||||
// Otherwise try to interpret the account as a keystore index
|
||||
index, err := strconv.Atoi(account)
|
||||
if err != nil {
|
||||
if err != nil || index < 0 {
|
||||
return accounts.Account{}, fmt.Errorf("invalid account address or index %q", account)
|
||||
}
|
||||
return accman.AccountByIndex(index)
|
||||
accs := ks.Accounts()
|
||||
if len(accs) <= index {
|
||||
return accounts.Account{}, fmt.Errorf("index %d higher than number of accounts %d", index, len(accs))
|
||||
}
|
||||
return accs[index], nil
|
||||
}
|
||||
|
||||
// MakeEtherbase retrieves the etherbase either from the directly specified
|
||||
// command line flags or from the keystore if CLI indexed.
|
||||
func MakeEtherbase(accman *accounts.Manager, ctx *cli.Context) common.Address {
|
||||
accounts := accman.Accounts()
|
||||
func MakeEtherbase(ks *keystore.KeyStore, ctx *cli.Context) common.Address {
|
||||
accounts := ks.Accounts()
|
||||
if !ctx.GlobalIsSet(EtherbaseFlag.Name) && len(accounts) == 0 {
|
||||
glog.V(logger.Error).Infoln("WARNING: No etherbase set and no accounts found as default")
|
||||
return common.Address{}
|
||||
@@ -613,7 +618,7 @@ func MakeEtherbase(accman *accounts.Manager, ctx *cli.Context) common.Address {
|
||||
return common.Address{}
|
||||
}
|
||||
// If the specified etherbase is a valid address, return it
|
||||
account, err := MakeAddress(accman, etherbase)
|
||||
account, err := MakeAddress(ks, etherbase)
|
||||
if err != nil {
|
||||
Fatalf("Option %q: %v", EtherbaseFlag.Name, err)
|
||||
}
|
||||
@@ -721,9 +726,10 @@ func RegisterEthService(ctx *cli.Context, stack *node.Node, extra []byte) {
|
||||
if networks > 1 {
|
||||
Fatalf("The %v flags are mutually exclusive", netFlags)
|
||||
}
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
|
||||
ethConf := ð.Config{
|
||||
Etherbase: MakeEtherbase(stack.AccountManager(), ctx),
|
||||
Etherbase: MakeEtherbase(ks, ctx),
|
||||
ChainConfig: MakeChainConfig(ctx, stack),
|
||||
FastSync: ctx.GlobalBool(FastSyncFlag.Name),
|
||||
LightMode: ctx.GlobalBool(LightModeFlag.Name),
|
||||
|
||||
@@ -209,10 +209,15 @@ func initialize() {
|
||||
nodeid = shh.NewIdentity()
|
||||
}
|
||||
|
||||
maxPeers := 80
|
||||
if *bootstrapMode {
|
||||
maxPeers = 800
|
||||
}
|
||||
|
||||
server = &p2p.Server{
|
||||
Config: p2p.Config{
|
||||
PrivateKey: nodeid,
|
||||
MaxPeers: 128,
|
||||
MaxPeers: maxPeers,
|
||||
Name: common.MakeName("whisper-go", "5.0"),
|
||||
Protocols: shh.Protocols(),
|
||||
ListenAddr: *argIP,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
FROM alpine:3.5
|
||||
|
||||
RUN \
|
||||
apk add --update go git make gcc musl-dev ca-certificates && \
|
||||
apk add --update go git make gcc musl-dev linux-headers ca-certificates && \
|
||||
git clone --depth 1 https://github.com/ethereum/go-ethereum && \
|
||||
(cd go-ethereum && make geth) && \
|
||||
cp go-ethereum/build/bin/geth /geth && \
|
||||
apk del go git make gcc musl-dev && \
|
||||
apk del go git make gcc musl-dev linux-headers && \
|
||||
rm -rf /go-ethereum && rm -rf /var/cache/apk/*
|
||||
|
||||
EXPOSE 8545
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
FROM alpine:3.5
|
||||
|
||||
RUN \
|
||||
apk add --update go git make gcc musl-dev ca-certificates && \
|
||||
apk add --update go git make gcc musl-dev linux-headers ca-certificates && \
|
||||
git clone --depth 1 --branch release/1.5 https://github.com/ethereum/go-ethereum && \
|
||||
(cd go-ethereum && make geth) && \
|
||||
cp go-ethereum/build/bin/geth /geth && \
|
||||
apk del go git make gcc musl-dev && \
|
||||
apk del go git make gcc musl-dev linux-headers && \
|
||||
rm -rf /go-ethereum && rm -rf /var/cache/apk/*
|
||||
|
||||
EXPOSE 8545
|
||||
|
||||
@@ -57,6 +57,7 @@ func WriteGenesisBlock(chainDb ethdb.Database, reader io.Reader) (*types.Block,
|
||||
Code string
|
||||
Storage map[string]string
|
||||
Balance string
|
||||
Nonce string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +70,8 @@ func WriteGenesisBlock(chainDb ethdb.Database, reader io.Reader) (*types.Block,
|
||||
for addr, account := range genesis.Alloc {
|
||||
address := common.HexToAddress(addr)
|
||||
statedb.AddBalance(address, common.String2Big(account.Balance))
|
||||
statedb.SetCode(address, common.Hex2Bytes(account.Code))
|
||||
statedb.SetCode(address, common.FromHex(account.Code))
|
||||
statedb.SetNonce(address, common.String2Big(account.Nonce).Uint64())
|
||||
for key, value := range account.Storage {
|
||||
statedb.SetState(address, common.HexToHash(key), common.HexToHash(value))
|
||||
}
|
||||
|
||||
@@ -82,10 +82,12 @@ func (ms *ManagedState) NewNonce(addr common.Address) uint64 {
|
||||
return uint64(len(account.nonces)-1) + account.nstart
|
||||
}
|
||||
|
||||
// GetNonce returns the canonical nonce for the managed or unmanaged account
|
||||
// GetNonce returns the canonical nonce for the managed or unmanaged account.
|
||||
//
|
||||
// Because GetNonce mutates the DB, we must take a write lock.
|
||||
func (ms *ManagedState) GetNonce(addr common.Address) uint64 {
|
||||
ms.mu.RLock()
|
||||
defer ms.mu.RUnlock()
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
|
||||
if ms.hasAccount(addr) {
|
||||
account := ms.getAccount(addr)
|
||||
|
||||
@@ -89,7 +89,7 @@ type TxPool struct {
|
||||
gasLimit func() *big.Int // The current gas limit function callback
|
||||
minGasPrice *big.Int
|
||||
eventMux *event.TypeMux
|
||||
events event.Subscription
|
||||
events *event.TypeMuxSubscription
|
||||
localTx *txSet
|
||||
signer types.Signer
|
||||
mu sync.RWMutex
|
||||
|
||||
@@ -157,7 +157,7 @@ var _baseCheck = map[OpCode]req{
|
||||
CALL: {7, Zero, 1},
|
||||
CALLCODE: {7, Zero, 1},
|
||||
DELEGATECALL: {6, Zero, 1},
|
||||
SUICIDE: {1, Zero, 0},
|
||||
SELFDESTRUCT: {1, Zero, 0},
|
||||
JUMPDEST: {0, params.JumpdestGas, 0},
|
||||
RETURN: {2, Zero, 0},
|
||||
PUSH1: {0, GasFastestStep, 1},
|
||||
|
||||
@@ -52,142 +52,149 @@ var defaultJumpTable = NewJumpTable()
|
||||
|
||||
func NewJumpTable() [256]operation {
|
||||
return [256]operation{
|
||||
STOP: {
|
||||
execute: opStop,
|
||||
gasCost: constGasFunc(new(big.Int)),
|
||||
validateStack: makeStackFunc(0, 0),
|
||||
halts: true,
|
||||
valid: true,
|
||||
},
|
||||
ADD: {
|
||||
execute: opAdd,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
valid: true,
|
||||
},
|
||||
SUB: {
|
||||
execute: opSub,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
MUL: {
|
||||
execute: opMul,
|
||||
gasCost: constGasFunc(GasFastStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
SUB: {
|
||||
execute: opSub,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
DIV: {
|
||||
execute: opDiv,
|
||||
gasCost: constGasFunc(GasFastStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
SDIV: {
|
||||
execute: opSdiv,
|
||||
gasCost: constGasFunc(GasFastStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
MOD: {
|
||||
execute: opMod,
|
||||
gasCost: constGasFunc(GasFastStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
SMOD: {
|
||||
execute: opSmod,
|
||||
gasCost: constGasFunc(GasFastStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
valid: true,
|
||||
},
|
||||
EXP: {
|
||||
execute: opExp,
|
||||
gasCost: gasExp,
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
valid: true,
|
||||
},
|
||||
SIGNEXTEND: {
|
||||
execute: opSignExtend,
|
||||
gasCost: constGasFunc(GasFastStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
valid: true,
|
||||
},
|
||||
NOT: {
|
||||
execute: opNot,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(1, 1),
|
||||
valid: true,
|
||||
},
|
||||
LT: {
|
||||
execute: opLt,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
valid: true,
|
||||
},
|
||||
GT: {
|
||||
execute: opGt,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
valid: true,
|
||||
},
|
||||
SLT: {
|
||||
execute: opSlt,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
valid: true,
|
||||
},
|
||||
SGT: {
|
||||
execute: opSgt,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
valid: true,
|
||||
},
|
||||
EQ: {
|
||||
execute: opEq,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
valid: true,
|
||||
},
|
||||
ISZERO: {
|
||||
execute: opIszero,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(1, 1),
|
||||
valid: true,
|
||||
},
|
||||
AND: {
|
||||
execute: opAnd,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
valid: true,
|
||||
},
|
||||
OR: {
|
||||
execute: opOr,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
valid: true,
|
||||
},
|
||||
XOR: {
|
||||
execute: opXor,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
valid: true,
|
||||
},
|
||||
BYTE: {
|
||||
execute: opByte,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
ADDMOD: {
|
||||
execute: opAddmod,
|
||||
gasCost: constGasFunc(GasMidStep),
|
||||
validateStack: makeStackFunc(3, 1),
|
||||
validateStack: makeStackFunc(3, -2),
|
||||
valid: true,
|
||||
},
|
||||
MULMOD: {
|
||||
execute: opMulmod,
|
||||
gasCost: constGasFunc(GasMidStep),
|
||||
validateStack: makeStackFunc(3, 1),
|
||||
validateStack: makeStackFunc(3, -2),
|
||||
valid: true,
|
||||
},
|
||||
EXP: {
|
||||
execute: opExp,
|
||||
gasCost: gasExp,
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
SIGNEXTEND: {
|
||||
execute: opSignExtend,
|
||||
gasCost: constGasFunc(GasFastStep),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
LT: {
|
||||
execute: opLt,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
GT: {
|
||||
execute: opGt,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
SLT: {
|
||||
execute: opSlt,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
SGT: {
|
||||
execute: opSgt,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
EQ: {
|
||||
execute: opEq,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
ISZERO: {
|
||||
execute: opIszero,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(1, 0),
|
||||
valid: true,
|
||||
},
|
||||
AND: {
|
||||
execute: opAnd,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
XOR: {
|
||||
execute: opXor,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
OR: {
|
||||
execute: opOr,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
NOT: {
|
||||
execute: opNot,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(1, 0),
|
||||
valid: true,
|
||||
},
|
||||
BYTE: {
|
||||
execute: opByte,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
valid: true,
|
||||
},
|
||||
SHA3: {
|
||||
execute: opSha3,
|
||||
gasCost: gasSha3,
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
validateStack: makeStackFunc(2, -1),
|
||||
memorySize: memorySha3,
|
||||
valid: true,
|
||||
},
|
||||
@@ -200,7 +207,7 @@ func NewJumpTable() [256]operation {
|
||||
BALANCE: {
|
||||
execute: opBalance,
|
||||
gasCost: gasBalance,
|
||||
validateStack: makeStackFunc(0, 1),
|
||||
validateStack: makeStackFunc(1, 0),
|
||||
valid: true,
|
||||
},
|
||||
ORIGIN: {
|
||||
@@ -224,7 +231,7 @@ func NewJumpTable() [256]operation {
|
||||
CALLDATALOAD: {
|
||||
execute: opCalldataLoad,
|
||||
gasCost: constGasFunc(GasFastestStep),
|
||||
validateStack: makeStackFunc(1, 1),
|
||||
validateStack: makeStackFunc(1, 0),
|
||||
valid: true,
|
||||
},
|
||||
CALLDATASIZE: {
|
||||
@@ -236,7 +243,7 @@ func NewJumpTable() [256]operation {
|
||||
CALLDATACOPY: {
|
||||
execute: opCalldataCopy,
|
||||
gasCost: gasCalldataCopy,
|
||||
validateStack: makeStackFunc(3, 1),
|
||||
validateStack: makeStackFunc(3, -3),
|
||||
memorySize: memoryCalldataCopy,
|
||||
valid: true,
|
||||
},
|
||||
@@ -246,36 +253,36 @@ func NewJumpTable() [256]operation {
|
||||
validateStack: makeStackFunc(0, 1),
|
||||
valid: true,
|
||||
},
|
||||
EXTCODESIZE: {
|
||||
execute: opExtCodeSize,
|
||||
gasCost: gasExtCodeSize,
|
||||
validateStack: makeStackFunc(1, 1),
|
||||
valid: true,
|
||||
},
|
||||
CODECOPY: {
|
||||
execute: opCodeCopy,
|
||||
gasCost: gasCodeCopy,
|
||||
validateStack: makeStackFunc(3, 0),
|
||||
validateStack: makeStackFunc(3, -3),
|
||||
memorySize: memoryCodeCopy,
|
||||
valid: true,
|
||||
},
|
||||
EXTCODECOPY: {
|
||||
execute: opExtCodeCopy,
|
||||
gasCost: gasExtCodeCopy,
|
||||
validateStack: makeStackFunc(4, 0),
|
||||
memorySize: memoryExtCodeCopy,
|
||||
valid: true,
|
||||
},
|
||||
GASPRICE: {
|
||||
execute: opGasprice,
|
||||
gasCost: constGasFunc(GasQuickStep),
|
||||
validateStack: makeStackFunc(0, 1),
|
||||
valid: true,
|
||||
},
|
||||
EXTCODESIZE: {
|
||||
execute: opExtCodeSize,
|
||||
gasCost: gasExtCodeSize,
|
||||
validateStack: makeStackFunc(1, 0),
|
||||
valid: true,
|
||||
},
|
||||
EXTCODECOPY: {
|
||||
execute: opExtCodeCopy,
|
||||
gasCost: gasExtCodeCopy,
|
||||
validateStack: makeStackFunc(4, -4),
|
||||
memorySize: memoryExtCodeCopy,
|
||||
valid: true,
|
||||
},
|
||||
BLOCKHASH: {
|
||||
execute: opBlockhash,
|
||||
gasCost: constGasFunc(GasExtStep),
|
||||
validateStack: makeStackFunc(1, 1),
|
||||
validateStack: makeStackFunc(1, 0),
|
||||
valid: true,
|
||||
},
|
||||
COINBASE: {
|
||||
@@ -311,20 +318,20 @@ func NewJumpTable() [256]operation {
|
||||
POP: {
|
||||
execute: opPop,
|
||||
gasCost: constGasFunc(GasQuickStep),
|
||||
validateStack: makeStackFunc(1, 0),
|
||||
validateStack: makeStackFunc(1, -1),
|
||||
valid: true,
|
||||
},
|
||||
MLOAD: {
|
||||
execute: opMload,
|
||||
gasCost: gasMLoad,
|
||||
validateStack: makeStackFunc(1, 1),
|
||||
validateStack: makeStackFunc(1, 0),
|
||||
memorySize: memoryMLoad,
|
||||
valid: true,
|
||||
},
|
||||
MSTORE: {
|
||||
execute: opMstore,
|
||||
gasCost: gasMStore,
|
||||
validateStack: makeStackFunc(2, 0),
|
||||
validateStack: makeStackFunc(2, -2),
|
||||
memorySize: memoryMStore,
|
||||
valid: true,
|
||||
},
|
||||
@@ -332,26 +339,34 @@ func NewJumpTable() [256]operation {
|
||||
execute: opMstore8,
|
||||
gasCost: gasMStore8,
|
||||
memorySize: memoryMStore8,
|
||||
validateStack: makeStackFunc(2, 0),
|
||||
validateStack: makeStackFunc(2, -2),
|
||||
|
||||
valid: true,
|
||||
},
|
||||
SLOAD: {
|
||||
execute: opSload,
|
||||
gasCost: gasSLoad,
|
||||
validateStack: makeStackFunc(1, 1),
|
||||
validateStack: makeStackFunc(1, 0),
|
||||
valid: true,
|
||||
},
|
||||
SSTORE: {
|
||||
execute: opSstore,
|
||||
gasCost: gasSStore,
|
||||
validateStack: makeStackFunc(2, 0),
|
||||
validateStack: makeStackFunc(2, -2),
|
||||
valid: true,
|
||||
},
|
||||
JUMPDEST: {
|
||||
execute: opJumpdest,
|
||||
gasCost: constGasFunc(params.JumpdestGas),
|
||||
validateStack: makeStackFunc(0, 0),
|
||||
JUMP: {
|
||||
execute: opJump,
|
||||
gasCost: constGasFunc(GasMidStep),
|
||||
validateStack: makeStackFunc(1, -1),
|
||||
jumps: true,
|
||||
valid: true,
|
||||
},
|
||||
JUMPI: {
|
||||
execute: opJumpi,
|
||||
gasCost: constGasFunc(GasSlowStep),
|
||||
validateStack: makeStackFunc(2, -2),
|
||||
jumps: true,
|
||||
valid: true,
|
||||
},
|
||||
PC: {
|
||||
@@ -372,199 +387,10 @@ func NewJumpTable() [256]operation {
|
||||
validateStack: makeStackFunc(0, 1),
|
||||
valid: true,
|
||||
},
|
||||
CREATE: {
|
||||
execute: opCreate,
|
||||
gasCost: gasCreate,
|
||||
validateStack: makeStackFunc(3, 1),
|
||||
memorySize: memoryCreate,
|
||||
valid: true,
|
||||
},
|
||||
CALL: {
|
||||
execute: opCall,
|
||||
gasCost: gasCall,
|
||||
validateStack: makeStackFunc(7, 1),
|
||||
memorySize: memoryCall,
|
||||
valid: true,
|
||||
},
|
||||
CALLCODE: {
|
||||
execute: opCallCode,
|
||||
gasCost: gasCallCode,
|
||||
validateStack: makeStackFunc(7, 1),
|
||||
memorySize: memoryCall,
|
||||
valid: true,
|
||||
},
|
||||
DELEGATECALL: {
|
||||
execute: opDelegateCall,
|
||||
gasCost: gasDelegateCall,
|
||||
validateStack: makeStackFunc(6, 1),
|
||||
memorySize: memoryDelegateCall,
|
||||
valid: true,
|
||||
},
|
||||
RETURN: {
|
||||
execute: opReturn,
|
||||
gasCost: gasReturn,
|
||||
validateStack: makeStackFunc(2, 0),
|
||||
memorySize: memoryReturn,
|
||||
halts: true,
|
||||
valid: true,
|
||||
},
|
||||
SUICIDE: {
|
||||
execute: opSuicide,
|
||||
gasCost: gasSuicide,
|
||||
validateStack: makeStackFunc(1, 0),
|
||||
halts: true,
|
||||
valid: true,
|
||||
},
|
||||
JUMP: {
|
||||
execute: opJump,
|
||||
gasCost: constGasFunc(GasMidStep),
|
||||
validateStack: makeStackFunc(1, 0),
|
||||
jumps: true,
|
||||
valid: true,
|
||||
},
|
||||
JUMPI: {
|
||||
execute: opJumpi,
|
||||
gasCost: constGasFunc(GasSlowStep),
|
||||
validateStack: makeStackFunc(2, 0),
|
||||
jumps: true,
|
||||
valid: true,
|
||||
},
|
||||
STOP: {
|
||||
execute: opStop,
|
||||
gasCost: constGasFunc(Zero),
|
||||
JUMPDEST: {
|
||||
execute: opJumpdest,
|
||||
gasCost: constGasFunc(params.JumpdestGas),
|
||||
validateStack: makeStackFunc(0, 0),
|
||||
halts: true,
|
||||
valid: true,
|
||||
},
|
||||
LOG0: {
|
||||
execute: makeLog(0),
|
||||
gasCost: makeGasLog(0),
|
||||
validateStack: makeStackFunc(2, 0),
|
||||
memorySize: memoryLog,
|
||||
valid: true,
|
||||
},
|
||||
LOG1: {
|
||||
execute: makeLog(1),
|
||||
gasCost: makeGasLog(1),
|
||||
validateStack: makeStackFunc(3, 0),
|
||||
memorySize: memoryLog,
|
||||
valid: true,
|
||||
},
|
||||
LOG2: {
|
||||
execute: makeLog(2),
|
||||
gasCost: makeGasLog(2),
|
||||
validateStack: makeStackFunc(4, 0),
|
||||
memorySize: memoryLog,
|
||||
valid: true,
|
||||
},
|
||||
LOG3: {
|
||||
execute: makeLog(3),
|
||||
gasCost: makeGasLog(3),
|
||||
validateStack: makeStackFunc(5, 0),
|
||||
memorySize: memoryLog,
|
||||
valid: true,
|
||||
},
|
||||
LOG4: {
|
||||
execute: makeLog(4),
|
||||
gasCost: makeGasLog(4),
|
||||
validateStack: makeStackFunc(6, 0),
|
||||
memorySize: memoryLog,
|
||||
valid: true,
|
||||
},
|
||||
SWAP1: {
|
||||
execute: makeSwap(1),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(2, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP2: {
|
||||
execute: makeSwap(2),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(3, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP3: {
|
||||
execute: makeSwap(3),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(4, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP4: {
|
||||
execute: makeSwap(4),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(5, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP5: {
|
||||
execute: makeSwap(5),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(6, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP6: {
|
||||
execute: makeSwap(6),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(7, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP7: {
|
||||
execute: makeSwap(7),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(8, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP8: {
|
||||
execute: makeSwap(8),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(9, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP9: {
|
||||
execute: makeSwap(9),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(10, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP10: {
|
||||
execute: makeSwap(10),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(11, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP11: {
|
||||
execute: makeSwap(11),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(12, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP12: {
|
||||
execute: makeSwap(12),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(13, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP13: {
|
||||
execute: makeSwap(13),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(14, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP14: {
|
||||
execute: makeSwap(14),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(15, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP15: {
|
||||
execute: makeSwap(15),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(16, 0),
|
||||
valid: true,
|
||||
},
|
||||
SWAP16: {
|
||||
execute: makeSwap(16),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeStackFunc(17, 0),
|
||||
valid: true,
|
||||
},
|
||||
PUSH1: {
|
||||
@@ -762,97 +588,271 @@ func NewJumpTable() [256]operation {
|
||||
DUP1: {
|
||||
execute: makeDup(1),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(1, 1),
|
||||
validateStack: makeDupStackFunc(1),
|
||||
valid: true,
|
||||
},
|
||||
DUP2: {
|
||||
execute: makeDup(2),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(2, 1),
|
||||
validateStack: makeDupStackFunc(2),
|
||||
valid: true,
|
||||
},
|
||||
DUP3: {
|
||||
execute: makeDup(3),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(3, 1),
|
||||
validateStack: makeDupStackFunc(3),
|
||||
valid: true,
|
||||
},
|
||||
DUP4: {
|
||||
execute: makeDup(4),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(4, 1),
|
||||
validateStack: makeDupStackFunc(4),
|
||||
valid: true,
|
||||
},
|
||||
DUP5: {
|
||||
execute: makeDup(5),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(5, 1),
|
||||
validateStack: makeDupStackFunc(5),
|
||||
valid: true,
|
||||
},
|
||||
DUP6: {
|
||||
execute: makeDup(6),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(6, 1),
|
||||
validateStack: makeDupStackFunc(6),
|
||||
valid: true,
|
||||
},
|
||||
DUP7: {
|
||||
execute: makeDup(7),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(7, 1),
|
||||
validateStack: makeDupStackFunc(7),
|
||||
valid: true,
|
||||
},
|
||||
DUP8: {
|
||||
execute: makeDup(8),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(8, 1),
|
||||
validateStack: makeDupStackFunc(8),
|
||||
valid: true,
|
||||
},
|
||||
DUP9: {
|
||||
execute: makeDup(9),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(9, 1),
|
||||
validateStack: makeDupStackFunc(9),
|
||||
valid: true,
|
||||
},
|
||||
DUP10: {
|
||||
execute: makeDup(10),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(10, 1),
|
||||
validateStack: makeDupStackFunc(10),
|
||||
valid: true,
|
||||
},
|
||||
DUP11: {
|
||||
execute: makeDup(11),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(11, 1),
|
||||
validateStack: makeDupStackFunc(11),
|
||||
valid: true,
|
||||
},
|
||||
DUP12: {
|
||||
execute: makeDup(12),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(12, 1),
|
||||
validateStack: makeDupStackFunc(12),
|
||||
valid: true,
|
||||
},
|
||||
DUP13: {
|
||||
execute: makeDup(13),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(13, 1),
|
||||
validateStack: makeDupStackFunc(13),
|
||||
valid: true,
|
||||
},
|
||||
DUP14: {
|
||||
execute: makeDup(14),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(14, 1),
|
||||
validateStack: makeDupStackFunc(14),
|
||||
valid: true,
|
||||
},
|
||||
DUP15: {
|
||||
execute: makeDup(15),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(15, 1),
|
||||
validateStack: makeDupStackFunc(15),
|
||||
valid: true,
|
||||
},
|
||||
DUP16: {
|
||||
execute: makeDup(16),
|
||||
gasCost: gasDup,
|
||||
validateStack: makeStackFunc(16, 1),
|
||||
validateStack: makeDupStackFunc(16),
|
||||
valid: true,
|
||||
},
|
||||
SWAP1: {
|
||||
execute: makeSwap(1),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(2),
|
||||
valid: true,
|
||||
},
|
||||
SWAP2: {
|
||||
execute: makeSwap(2),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(3),
|
||||
valid: true,
|
||||
},
|
||||
SWAP3: {
|
||||
execute: makeSwap(3),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(4),
|
||||
valid: true,
|
||||
},
|
||||
SWAP4: {
|
||||
execute: makeSwap(4),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(5),
|
||||
valid: true,
|
||||
},
|
||||
SWAP5: {
|
||||
execute: makeSwap(5),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(6),
|
||||
valid: true,
|
||||
},
|
||||
SWAP6: {
|
||||
execute: makeSwap(6),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(7),
|
||||
valid: true,
|
||||
},
|
||||
SWAP7: {
|
||||
execute: makeSwap(7),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(8),
|
||||
valid: true,
|
||||
},
|
||||
SWAP8: {
|
||||
execute: makeSwap(8),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(9),
|
||||
valid: true,
|
||||
},
|
||||
SWAP9: {
|
||||
execute: makeSwap(9),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(10),
|
||||
valid: true,
|
||||
},
|
||||
SWAP10: {
|
||||
execute: makeSwap(10),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(11),
|
||||
valid: true,
|
||||
},
|
||||
SWAP11: {
|
||||
execute: makeSwap(11),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(12),
|
||||
valid: true,
|
||||
},
|
||||
SWAP12: {
|
||||
execute: makeSwap(12),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(13),
|
||||
valid: true,
|
||||
},
|
||||
SWAP13: {
|
||||
execute: makeSwap(13),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(14),
|
||||
valid: true,
|
||||
},
|
||||
SWAP14: {
|
||||
execute: makeSwap(14),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(15),
|
||||
valid: true,
|
||||
},
|
||||
SWAP15: {
|
||||
execute: makeSwap(15),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(16),
|
||||
valid: true,
|
||||
},
|
||||
SWAP16: {
|
||||
execute: makeSwap(16),
|
||||
gasCost: gasSwap,
|
||||
validateStack: makeSwapStackFunc(17),
|
||||
valid: true,
|
||||
},
|
||||
LOG0: {
|
||||
execute: makeLog(0),
|
||||
gasCost: makeGasLog(0),
|
||||
validateStack: makeStackFunc(2, -2),
|
||||
memorySize: memoryLog,
|
||||
valid: true,
|
||||
},
|
||||
LOG1: {
|
||||
execute: makeLog(1),
|
||||
gasCost: makeGasLog(1),
|
||||
validateStack: makeStackFunc(3, -3),
|
||||
memorySize: memoryLog,
|
||||
valid: true,
|
||||
},
|
||||
LOG2: {
|
||||
execute: makeLog(2),
|
||||
gasCost: makeGasLog(2),
|
||||
validateStack: makeStackFunc(4, -4),
|
||||
memorySize: memoryLog,
|
||||
valid: true,
|
||||
},
|
||||
LOG3: {
|
||||
execute: makeLog(3),
|
||||
gasCost: makeGasLog(3),
|
||||
validateStack: makeStackFunc(5, -5),
|
||||
memorySize: memoryLog,
|
||||
valid: true,
|
||||
},
|
||||
LOG4: {
|
||||
execute: makeLog(4),
|
||||
gasCost: makeGasLog(4),
|
||||
validateStack: makeStackFunc(6, -6),
|
||||
memorySize: memoryLog,
|
||||
valid: true,
|
||||
},
|
||||
CREATE: {
|
||||
execute: opCreate,
|
||||
gasCost: gasCreate,
|
||||
validateStack: makeStackFunc(3, -2),
|
||||
memorySize: memoryCreate,
|
||||
valid: true,
|
||||
},
|
||||
CALL: {
|
||||
execute: opCall,
|
||||
gasCost: gasCall,
|
||||
validateStack: makeStackFunc(7, -6),
|
||||
memorySize: memoryCall,
|
||||
valid: true,
|
||||
},
|
||||
CALLCODE: {
|
||||
execute: opCallCode,
|
||||
gasCost: gasCallCode,
|
||||
validateStack: makeStackFunc(7, -6),
|
||||
memorySize: memoryCall,
|
||||
valid: true,
|
||||
},
|
||||
RETURN: {
|
||||
execute: opReturn,
|
||||
gasCost: gasReturn,
|
||||
validateStack: makeStackFunc(2, -2),
|
||||
memorySize: memoryReturn,
|
||||
halts: true,
|
||||
valid: true,
|
||||
},
|
||||
DELEGATECALL: {
|
||||
execute: opDelegateCall,
|
||||
gasCost: gasDelegateCall,
|
||||
validateStack: makeStackFunc(6, -5),
|
||||
memorySize: memoryDelegateCall,
|
||||
valid: true,
|
||||
},
|
||||
SELFDESTRUCT: {
|
||||
execute: opSuicide,
|
||||
gasCost: gasSuicide,
|
||||
validateStack: makeStackFunc(1, -1),
|
||||
halts: true,
|
||||
valid: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ const (
|
||||
RETURN
|
||||
DELEGATECALL
|
||||
|
||||
SUICIDE = 0xff
|
||||
SELFDESTRUCT = 0xff
|
||||
)
|
||||
|
||||
// Since the opcodes aren't all in order we can't use a regular slice
|
||||
@@ -355,7 +355,7 @@ var opCodeToString = map[OpCode]string{
|
||||
RETURN: "RETURN",
|
||||
CALLCODE: "CALLCODE",
|
||||
DELEGATECALL: "DELEGATECALL",
|
||||
SUICIDE: "SUICIDE",
|
||||
SELFDESTRUCT: "SELFDESTRUCT",
|
||||
|
||||
PUSH: "PUSH",
|
||||
DUP: "DUP",
|
||||
@@ -501,7 +501,7 @@ var stringToOp = map[string]OpCode{
|
||||
"CALL": CALL,
|
||||
"RETURN": RETURN,
|
||||
"CALLCODE": CALLCODE,
|
||||
"SUICIDE": SUICIDE,
|
||||
"SELFDESTRUCT": SELFDESTRUCT,
|
||||
}
|
||||
|
||||
func StringToOp(str string) OpCode {
|
||||
|
||||
@@ -6,15 +6,23 @@ import (
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
func makeStackFunc(pop, push int) stackValidationFunc {
|
||||
func makeStackFunc(pop, diff int) stackValidationFunc {
|
||||
return func(stack *Stack) error {
|
||||
if err := stack.require(pop); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if push > 0 && int64(stack.len()-pop+push) > params.StackLimit.Int64() {
|
||||
return fmt.Errorf("stack limit reached %d (%d)", stack.len(), params.StackLimit.Int64())
|
||||
if int64(stack.len()+diff) > params.StackLimit.Int64() {
|
||||
return fmt.Errorf("stack limit reached %d (%d)", stack.len(), params.StackLimit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func makeDupStackFunc(n int) stackValidationFunc {
|
||||
return makeStackFunc(n, 1)
|
||||
}
|
||||
|
||||
func makeSwapStackFunc(n int) stackValidationFunc {
|
||||
return makeStackFunc(n, 0)
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ func (evm *Interpreter) Run(contract *Contract, input []byte) (ret []byte, err e
|
||||
}
|
||||
|
||||
// The Interpreter main run loop (contextual). This loop runs until either an
|
||||
// explicit STOP, RETURN or SUICIDE is executed, an error accured during
|
||||
// explicit STOP, RETURN or SELFDESTRUCT is executed, an error occurred during
|
||||
// the execution of one of the operations or until the evm.done is set by
|
||||
// the parent context.Context.
|
||||
for atomic.LoadInt32(&evm.env.abort) == 0 {
|
||||
|
||||
@@ -361,15 +361,15 @@ func (s *Ethereum) ResetWithGenesisBlock(gb *types.Block) {
|
||||
}
|
||||
|
||||
func (s *Ethereum) Etherbase() (eb common.Address, err error) {
|
||||
eb = s.etherbase
|
||||
if (eb == common.Address{}) {
|
||||
firstAccount, err := s.AccountManager().AccountByIndex(0)
|
||||
eb = firstAccount.Address
|
||||
if err != nil {
|
||||
return eb, fmt.Errorf("etherbase address must be explicitly specified")
|
||||
if s.etherbase != (common.Address{}) {
|
||||
return s.etherbase, nil
|
||||
}
|
||||
if wallets := s.AccountManager().Wallets(); len(wallets) > 0 {
|
||||
if accounts := wallets[0].Accounts(); len(accounts) > 0 {
|
||||
return accounts[0].Address, nil
|
||||
}
|
||||
}
|
||||
return eb, nil
|
||||
return common.Address{}, fmt.Errorf("etherbase address must be explicitly specified")
|
||||
}
|
||||
|
||||
// set in js console via admin interface or wrapper from cli flags
|
||||
|
||||
@@ -74,7 +74,7 @@ type subscription struct {
|
||||
// subscription which match the subscription criteria.
|
||||
type EventSystem struct {
|
||||
mux *event.TypeMux
|
||||
sub event.Subscription
|
||||
sub *event.TypeMuxSubscription
|
||||
backend Backend
|
||||
lightMode bool
|
||||
lastHead *types.Header
|
||||
@@ -277,7 +277,7 @@ func (es *EventSystem) SubscribePendingTxEvents(hashes chan common.Hash) *Subscr
|
||||
type filterIndex map[Type]map[rpc.ID]*subscription
|
||||
|
||||
// broadcast event to filters that match criteria.
|
||||
func (es *EventSystem) broadcast(filters filterIndex, ev *event.Event) {
|
||||
func (es *EventSystem) broadcast(filters filterIndex, ev *event.TypeMuxEvent) {
|
||||
if ev == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -78,8 +78,8 @@ type ProtocolManager struct {
|
||||
SubProtocols []p2p.Protocol
|
||||
|
||||
eventMux *event.TypeMux
|
||||
txSub event.Subscription
|
||||
minedBlockSub event.Subscription
|
||||
txSub *event.TypeMuxSubscription
|
||||
minedBlockSub *event.TypeMuxSubscription
|
||||
|
||||
// channels for fetcher, syncer, txsyncLoop
|
||||
newPeerCh chan *peer
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package event implements an event multiplexer.
|
||||
// Package event deals with subscriptions to real-time events.
|
||||
package event
|
||||
|
||||
import (
|
||||
@@ -25,33 +25,22 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Event is a time-tagged notification pushed to subscribers.
|
||||
type Event struct {
|
||||
// TypeMuxEvent is a time-tagged notification pushed to subscribers.
|
||||
type TypeMuxEvent struct {
|
||||
Time time.Time
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
// Subscription is implemented by event subscriptions.
|
||||
type Subscription interface {
|
||||
// Chan returns a channel that carries events.
|
||||
// Implementations should return the same channel
|
||||
// for any subsequent calls to Chan.
|
||||
Chan() <-chan *Event
|
||||
|
||||
// Unsubscribe stops delivery of events to a subscription.
|
||||
// The event channel is closed.
|
||||
// Unsubscribe can be called more than once.
|
||||
Unsubscribe()
|
||||
}
|
||||
|
||||
// A TypeMux dispatches events to registered receivers. Receivers can be
|
||||
// registered to handle events of certain type. Any operation
|
||||
// called after mux is stopped will return ErrMuxClosed.
|
||||
//
|
||||
// The zero value is ready to use.
|
||||
//
|
||||
// Deprecated: use Feed
|
||||
type TypeMux struct {
|
||||
mutex sync.RWMutex
|
||||
subm map[reflect.Type][]*muxsub
|
||||
subm map[reflect.Type][]*TypeMuxSubscription
|
||||
stopped bool
|
||||
}
|
||||
|
||||
@@ -61,7 +50,7 @@ var ErrMuxClosed = errors.New("event: mux closed")
|
||||
// Subscribe creates a subscription for events of the given types. The
|
||||
// subscription's channel is closed when it is unsubscribed
|
||||
// or the mux is closed.
|
||||
func (mux *TypeMux) Subscribe(types ...interface{}) Subscription {
|
||||
func (mux *TypeMux) Subscribe(types ...interface{}) *TypeMuxSubscription {
|
||||
sub := newsub(mux)
|
||||
mux.mutex.Lock()
|
||||
defer mux.mutex.Unlock()
|
||||
@@ -72,7 +61,7 @@ func (mux *TypeMux) Subscribe(types ...interface{}) Subscription {
|
||||
close(sub.postC)
|
||||
} else {
|
||||
if mux.subm == nil {
|
||||
mux.subm = make(map[reflect.Type][]*muxsub)
|
||||
mux.subm = make(map[reflect.Type][]*TypeMuxSubscription)
|
||||
}
|
||||
for _, t := range types {
|
||||
rtyp := reflect.TypeOf(t)
|
||||
@@ -80,7 +69,7 @@ func (mux *TypeMux) Subscribe(types ...interface{}) Subscription {
|
||||
if find(oldsubs, sub) != -1 {
|
||||
panic(fmt.Sprintf("event: duplicate type %s in Subscribe", rtyp))
|
||||
}
|
||||
subs := make([]*muxsub, len(oldsubs)+1)
|
||||
subs := make([]*TypeMuxSubscription, len(oldsubs)+1)
|
||||
copy(subs, oldsubs)
|
||||
subs[len(oldsubs)] = sub
|
||||
mux.subm[rtyp] = subs
|
||||
@@ -92,7 +81,7 @@ func (mux *TypeMux) Subscribe(types ...interface{}) Subscription {
|
||||
// Post sends an event to all receivers registered for the given type.
|
||||
// It returns ErrMuxClosed if the mux has been stopped.
|
||||
func (mux *TypeMux) Post(ev interface{}) error {
|
||||
event := &Event{
|
||||
event := &TypeMuxEvent{
|
||||
Time: time.Now(),
|
||||
Data: ev,
|
||||
}
|
||||
@@ -125,7 +114,7 @@ func (mux *TypeMux) Stop() {
|
||||
mux.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (mux *TypeMux) del(s *muxsub) {
|
||||
func (mux *TypeMux) del(s *TypeMuxSubscription) {
|
||||
mux.mutex.Lock()
|
||||
for typ, subs := range mux.subm {
|
||||
if pos := find(subs, s); pos >= 0 {
|
||||
@@ -139,7 +128,7 @@ func (mux *TypeMux) del(s *muxsub) {
|
||||
s.mux.mutex.Unlock()
|
||||
}
|
||||
|
||||
func find(slice []*muxsub, item *muxsub) int {
|
||||
func find(slice []*TypeMuxSubscription, item *TypeMuxSubscription) int {
|
||||
for i, v := range slice {
|
||||
if v == item {
|
||||
return i
|
||||
@@ -148,14 +137,15 @@ func find(slice []*muxsub, item *muxsub) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func posdelete(slice []*muxsub, pos int) []*muxsub {
|
||||
news := make([]*muxsub, len(slice)-1)
|
||||
func posdelete(slice []*TypeMuxSubscription, pos int) []*TypeMuxSubscription {
|
||||
news := make([]*TypeMuxSubscription, len(slice)-1)
|
||||
copy(news[:pos], slice[:pos])
|
||||
copy(news[pos:], slice[pos+1:])
|
||||
return news
|
||||
}
|
||||
|
||||
type muxsub struct {
|
||||
// TypeMuxSubscription is a subscription established through TypeMux.
|
||||
type TypeMuxSubscription struct {
|
||||
mux *TypeMux
|
||||
created time.Time
|
||||
closeMu sync.Mutex
|
||||
@@ -166,13 +156,13 @@ type muxsub struct {
|
||||
// postC can be set to nil without affecting the return value of
|
||||
// Chan.
|
||||
postMu sync.RWMutex
|
||||
readC <-chan *Event
|
||||
postC chan<- *Event
|
||||
readC <-chan *TypeMuxEvent
|
||||
postC chan<- *TypeMuxEvent
|
||||
}
|
||||
|
||||
func newsub(mux *TypeMux) *muxsub {
|
||||
c := make(chan *Event)
|
||||
return &muxsub{
|
||||
func newsub(mux *TypeMux) *TypeMuxSubscription {
|
||||
c := make(chan *TypeMuxEvent)
|
||||
return &TypeMuxSubscription{
|
||||
mux: mux,
|
||||
created: time.Now(),
|
||||
readC: c,
|
||||
@@ -181,16 +171,16 @@ func newsub(mux *TypeMux) *muxsub {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *muxsub) Chan() <-chan *Event {
|
||||
func (s *TypeMuxSubscription) Chan() <-chan *TypeMuxEvent {
|
||||
return s.readC
|
||||
}
|
||||
|
||||
func (s *muxsub) Unsubscribe() {
|
||||
func (s *TypeMuxSubscription) Unsubscribe() {
|
||||
s.mux.del(s)
|
||||
s.closewait()
|
||||
}
|
||||
|
||||
func (s *muxsub) closewait() {
|
||||
func (s *TypeMuxSubscription) closewait() {
|
||||
s.closeMu.Lock()
|
||||
defer s.closeMu.Unlock()
|
||||
if s.closed {
|
||||
@@ -205,7 +195,7 @@ func (s *muxsub) closewait() {
|
||||
s.postMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *muxsub) deliver(event *Event) {
|
||||
func (s *TypeMuxSubscription) deliver(event *TypeMuxEvent) {
|
||||
// Short circuit delivery if stale event
|
||||
if s.created.After(event.Time) {
|
||||
return
|
||||
|
||||
@@ -149,16 +149,34 @@ func emptySubscriber(mux *TypeMux, types ...interface{}) {
|
||||
}()
|
||||
}
|
||||
|
||||
func BenchmarkPost3(b *testing.B) {
|
||||
var mux = new(TypeMux)
|
||||
defer mux.Stop()
|
||||
emptySubscriber(mux, testEvent(0))
|
||||
emptySubscriber(mux, testEvent(0))
|
||||
emptySubscriber(mux, testEvent(0))
|
||||
func BenchmarkPost1000(b *testing.B) {
|
||||
var (
|
||||
mux = new(TypeMux)
|
||||
subscribed, done sync.WaitGroup
|
||||
nsubs = 1000
|
||||
)
|
||||
subscribed.Add(nsubs)
|
||||
done.Add(nsubs)
|
||||
for i := 0; i < nsubs; i++ {
|
||||
go func() {
|
||||
s := mux.Subscribe(testEvent(0))
|
||||
subscribed.Done()
|
||||
for range s.Chan() {
|
||||
}
|
||||
done.Done()
|
||||
}()
|
||||
}
|
||||
subscribed.Wait()
|
||||
|
||||
// The actual benchmark.
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mux.Post(testEvent(0))
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
mux.Stop()
|
||||
done.Wait()
|
||||
}
|
||||
|
||||
func BenchmarkPostConcurrent(b *testing.B) {
|
||||
|
||||
73
event/example_feed_test.go
Normal file
73
event/example_feed_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package event_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
)
|
||||
|
||||
func ExampleFeed_acknowledgedEvents() {
|
||||
// This example shows how the return value of Send can be used for request/reply
|
||||
// interaction between event consumers and producers.
|
||||
var feed event.Feed
|
||||
type ackedEvent struct {
|
||||
i int
|
||||
ack chan<- struct{}
|
||||
}
|
||||
|
||||
// Consumers wait for events on the feed and acknowledge processing.
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
for i := 0; i < 3; i++ {
|
||||
ch := make(chan ackedEvent, 100)
|
||||
sub := feed.Subscribe(ch)
|
||||
go func() {
|
||||
defer sub.Unsubscribe()
|
||||
for {
|
||||
select {
|
||||
case ev := <-ch:
|
||||
fmt.Println(ev.i) // "process" the event
|
||||
ev.ack <- struct{}{}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// The producer sends values of type ackedEvent with increasing values of i.
|
||||
// It waits for all consumers to acknowledge before sending the next event.
|
||||
for i := 0; i < 3; i++ {
|
||||
acksignal := make(chan struct{})
|
||||
n := feed.Send(ackedEvent{i, acksignal})
|
||||
for ack := 0; ack < n; ack++ {
|
||||
<-acksignal
|
||||
}
|
||||
}
|
||||
// Output:
|
||||
// 0
|
||||
// 0
|
||||
// 0
|
||||
// 1
|
||||
// 1
|
||||
// 1
|
||||
// 2
|
||||
// 2
|
||||
// 2
|
||||
}
|
||||
128
event/example_scope_test.go
Normal file
128
event/example_scope_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package event_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
)
|
||||
|
||||
// This example demonstrates how SubscriptionScope can be used to control the lifetime of
|
||||
// subscriptions.
|
||||
//
|
||||
// Our example program consists of two servers, each of which performs a calculation when
|
||||
// requested. The servers also allow subscribing to results of all computations.
|
||||
type divServer struct{ results event.Feed }
|
||||
type mulServer struct{ results event.Feed }
|
||||
|
||||
func (s *divServer) do(a, b int) int {
|
||||
r := a / b
|
||||
s.results.Send(r)
|
||||
return r
|
||||
}
|
||||
|
||||
func (s *mulServer) do(a, b int) int {
|
||||
r := a * b
|
||||
s.results.Send(r)
|
||||
return r
|
||||
}
|
||||
|
||||
// The servers are contained in an App. The app controls the servers and exposes them
|
||||
// through its API.
|
||||
type App struct {
|
||||
divServer
|
||||
mulServer
|
||||
scope event.SubscriptionScope
|
||||
}
|
||||
|
||||
func (s *App) Calc(op byte, a, b int) int {
|
||||
switch op {
|
||||
case '/':
|
||||
return s.divServer.do(a, b)
|
||||
case '*':
|
||||
return s.mulServer.do(a, b)
|
||||
default:
|
||||
panic("invalid op")
|
||||
}
|
||||
}
|
||||
|
||||
// The app's SubscribeResults method starts sending calculation results to the given
|
||||
// channel. Subscriptions created through this method are tied to the lifetime of the App
|
||||
// because they are registered in the scope.
|
||||
func (s *App) SubscribeResults(op byte, ch chan<- int) event.Subscription {
|
||||
switch op {
|
||||
case '/':
|
||||
return s.scope.Track(s.divServer.results.Subscribe(ch))
|
||||
case '*':
|
||||
return s.scope.Track(s.mulServer.results.Subscribe(ch))
|
||||
default:
|
||||
panic("invalid op")
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the App, closing all subscriptions created through SubscribeResults.
|
||||
func (s *App) Stop() {
|
||||
s.scope.Close()
|
||||
}
|
||||
|
||||
func ExampleSubscriptionScope() {
|
||||
// Create the app.
|
||||
var (
|
||||
app App
|
||||
wg sync.WaitGroup
|
||||
divs = make(chan int)
|
||||
muls = make(chan int)
|
||||
)
|
||||
|
||||
// Run a subscriber in the background.
|
||||
divsub := app.SubscribeResults('/', divs)
|
||||
mulsub := app.SubscribeResults('*', muls)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer fmt.Println("subscriber exited")
|
||||
defer divsub.Unsubscribe()
|
||||
defer mulsub.Unsubscribe()
|
||||
for {
|
||||
select {
|
||||
case result := <-divs:
|
||||
fmt.Println("division happened:", result)
|
||||
case result := <-muls:
|
||||
fmt.Println("multiplication happened:", result)
|
||||
case <-divsub.Err():
|
||||
return
|
||||
case <-mulsub.Err():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Interact with the app.
|
||||
app.Calc('/', 22, 11)
|
||||
app.Calc('*', 3, 4)
|
||||
|
||||
// Stop the app. This shuts down the subscriptions, causing the subscriber to exit.
|
||||
app.Stop()
|
||||
wg.Wait()
|
||||
|
||||
// Output:
|
||||
// division happened: 2
|
||||
// multiplication happened: 12
|
||||
// subscriber exited
|
||||
}
|
||||
56
event/example_subscription_test.go
Normal file
56
event/example_subscription_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package event_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
)
|
||||
|
||||
func ExampleNewSubscription() {
|
||||
// Create a subscription that sends 10 integers on ch.
|
||||
ch := make(chan int)
|
||||
sub := event.NewSubscription(func(quit <-chan struct{}) error {
|
||||
for i := 0; i < 10; i++ {
|
||||
select {
|
||||
case ch <- i:
|
||||
case <-quit:
|
||||
fmt.Println("unsubscribed")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// This is the consumer. It reads 5 integers, then aborts the subscription.
|
||||
// Note that Unsubscribe waits until the producer has shut down.
|
||||
for i := range ch {
|
||||
fmt.Println(i)
|
||||
if i == 4 {
|
||||
sub.Unsubscribe()
|
||||
break
|
||||
}
|
||||
}
|
||||
// Output:
|
||||
// 0
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// unsubscribed
|
||||
}
|
||||
243
event/feed.go
Normal file
243
event/feed.go
Normal file
@@ -0,0 +1,243 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var errBadChannel = errors.New("event: Subscribe argument does not have sendable channel type")
|
||||
|
||||
// Feed implements one-to-many subscriptions where the carrier of events is a channel.
|
||||
// Values sent to a Feed are delivered to all subscribed channels simultaneously.
|
||||
//
|
||||
// Feeds can only be used with a single type. The type is determined by the first Send or
|
||||
// Subscribe operation. Subsequent calls to these methods panic if the type does not
|
||||
// match.
|
||||
//
|
||||
// The zero value is ready to use.
|
||||
type Feed struct {
|
||||
once sync.Once // ensures that init only runs once
|
||||
sendLock chan struct{} // sendLock has a one-element buffer and is empty when held.It protects sendCases.
|
||||
removeSub chan interface{} // interrupts Send
|
||||
sendCases caseList // the active set of select cases used by Send
|
||||
|
||||
// The inbox holds newly subscribed channels until they are added to sendCases.
|
||||
mu sync.Mutex
|
||||
inbox caseList
|
||||
etype reflect.Type
|
||||
closed bool
|
||||
}
|
||||
|
||||
// This is the index of the first actual subscription channel in sendCases.
|
||||
// sendCases[0] is a SelectRecv case for the removeSub channel.
|
||||
const firstSubSendCase = 1
|
||||
|
||||
type feedTypeError struct {
|
||||
got, want reflect.Type
|
||||
op string
|
||||
}
|
||||
|
||||
func (e feedTypeError) Error() string {
|
||||
return "event: wrong type in " + e.op + " got " + e.got.String() + ", want " + e.want.String()
|
||||
}
|
||||
|
||||
func (f *Feed) init() {
|
||||
f.removeSub = make(chan interface{})
|
||||
f.sendLock = make(chan struct{}, 1)
|
||||
f.sendLock <- struct{}{}
|
||||
f.sendCases = caseList{{Chan: reflect.ValueOf(f.removeSub), Dir: reflect.SelectRecv}}
|
||||
}
|
||||
|
||||
// Subscribe adds a channel to the feed. Future sends will be delivered on the channel
|
||||
// until the subscription is canceled. All channels added must have the same element type.
|
||||
//
|
||||
// The channel should have ample buffer space to avoid blocking other subscribers.
|
||||
// Slow subscribers are not dropped.
|
||||
func (f *Feed) Subscribe(channel interface{}) Subscription {
|
||||
f.once.Do(f.init)
|
||||
|
||||
chanval := reflect.ValueOf(channel)
|
||||
chantyp := chanval.Type()
|
||||
if chantyp.Kind() != reflect.Chan || chantyp.ChanDir()&reflect.SendDir == 0 {
|
||||
panic(errBadChannel)
|
||||
}
|
||||
sub := &feedSub{feed: f, channel: chanval, err: make(chan error, 1)}
|
||||
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if !f.typecheck(chantyp.Elem()) {
|
||||
panic(feedTypeError{op: "Subscribe", got: chantyp, want: reflect.ChanOf(reflect.SendDir, f.etype)})
|
||||
}
|
||||
// Add the select case to the inbox.
|
||||
// The next Send will add it to f.sendCases.
|
||||
cas := reflect.SelectCase{Dir: reflect.SelectSend, Chan: chanval}
|
||||
f.inbox = append(f.inbox, cas)
|
||||
return sub
|
||||
}
|
||||
|
||||
// note: callers must hold f.mu
|
||||
func (f *Feed) typecheck(typ reflect.Type) bool {
|
||||
if f.etype == nil {
|
||||
f.etype = typ
|
||||
return true
|
||||
}
|
||||
return f.etype == typ
|
||||
}
|
||||
|
||||
func (f *Feed) remove(sub *feedSub) {
|
||||
// Delete from inbox first, which covers channels
|
||||
// that have not been added to f.sendCases yet.
|
||||
ch := sub.channel.Interface()
|
||||
f.mu.Lock()
|
||||
index := f.inbox.find(ch)
|
||||
if index != -1 {
|
||||
f.inbox = f.inbox.delete(index)
|
||||
f.mu.Unlock()
|
||||
return
|
||||
}
|
||||
f.mu.Unlock()
|
||||
|
||||
select {
|
||||
case f.removeSub <- ch:
|
||||
// Send will remove the channel from f.sendCases.
|
||||
case <-f.sendLock:
|
||||
// No Send is in progress, delete the channel now that we have the send lock.
|
||||
f.sendCases = f.sendCases.delete(f.sendCases.find(ch))
|
||||
f.sendLock <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Send delivers to all subscribed channels simultaneously.
|
||||
// It returns the number of subscribers that the value was sent to.
|
||||
func (f *Feed) Send(value interface{}) (nsent int) {
|
||||
f.once.Do(f.init)
|
||||
<-f.sendLock
|
||||
|
||||
// Add new cases from the inbox after taking the send lock.
|
||||
f.mu.Lock()
|
||||
f.sendCases = append(f.sendCases, f.inbox...)
|
||||
f.inbox = nil
|
||||
f.mu.Unlock()
|
||||
|
||||
// Set the sent value on all channels.
|
||||
rvalue := reflect.ValueOf(value)
|
||||
if !f.typecheck(rvalue.Type()) {
|
||||
f.sendLock <- struct{}{}
|
||||
panic(feedTypeError{op: "Send", got: rvalue.Type(), want: f.etype})
|
||||
}
|
||||
for i := firstSubSendCase; i < len(f.sendCases); i++ {
|
||||
f.sendCases[i].Send = rvalue
|
||||
}
|
||||
|
||||
// Send until all channels except removeSub have been chosen.
|
||||
cases := f.sendCases
|
||||
for {
|
||||
// Fast path: try sending without blocking before adding to the select set.
|
||||
// This should usually succeed if subscribers are fast enough and have free
|
||||
// buffer space.
|
||||
for i := firstSubSendCase; i < len(cases); i++ {
|
||||
if cases[i].Chan.TrySend(rvalue) {
|
||||
nsent++
|
||||
cases = cases.deactivate(i)
|
||||
i--
|
||||
}
|
||||
}
|
||||
if len(cases) == firstSubSendCase {
|
||||
break
|
||||
}
|
||||
// Select on all the receivers, waiting for them to unblock.
|
||||
chosen, recv, _ := reflect.Select(cases)
|
||||
if chosen == 0 /* <-f.removeSub */ {
|
||||
index := f.sendCases.find(recv.Interface())
|
||||
f.sendCases = f.sendCases.delete(index)
|
||||
if index >= 0 && index < len(cases) {
|
||||
cases = f.sendCases[:len(cases)-1]
|
||||
}
|
||||
} else {
|
||||
cases = cases.deactivate(chosen)
|
||||
nsent++
|
||||
}
|
||||
}
|
||||
|
||||
// Forget about the sent value and hand off the send lock.
|
||||
for i := firstSubSendCase; i < len(f.sendCases); i++ {
|
||||
f.sendCases[i].Send = reflect.Value{}
|
||||
}
|
||||
f.sendLock <- struct{}{}
|
||||
return nsent
|
||||
}
|
||||
|
||||
type feedSub struct {
|
||||
feed *Feed
|
||||
channel reflect.Value
|
||||
errOnce sync.Once
|
||||
err chan error
|
||||
}
|
||||
|
||||
func (sub *feedSub) Unsubscribe() {
|
||||
sub.errOnce.Do(func() {
|
||||
sub.feed.remove(sub)
|
||||
close(sub.err)
|
||||
})
|
||||
}
|
||||
|
||||
func (sub *feedSub) Err() <-chan error {
|
||||
return sub.err
|
||||
}
|
||||
|
||||
type caseList []reflect.SelectCase
|
||||
|
||||
// find returns the index of a case containing the given channel.
|
||||
func (cs caseList) find(channel interface{}) int {
|
||||
for i, cas := range cs {
|
||||
if cas.Chan.Interface() == channel {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// delete removes the given case from cs.
|
||||
func (cs caseList) delete(index int) caseList {
|
||||
return append(cs[:index], cs[index+1:]...)
|
||||
}
|
||||
|
||||
// deactivate moves the case at index into the non-accessible portion of the cs slice.
|
||||
func (cs caseList) deactivate(index int) caseList {
|
||||
last := len(cs) - 1
|
||||
cs[index], cs[last] = cs[last], cs[index]
|
||||
return cs[:last]
|
||||
}
|
||||
|
||||
// func (cs caseList) String() string {
|
||||
// s := "["
|
||||
// for i, cas := range cs {
|
||||
// if i != 0 {
|
||||
// s += ", "
|
||||
// }
|
||||
// switch cas.Dir {
|
||||
// case reflect.SelectSend:
|
||||
// s += fmt.Sprintf("%v<-", cas.Chan.Interface())
|
||||
// case reflect.SelectRecv:
|
||||
// s += fmt.Sprintf("<-%v", cas.Chan.Interface())
|
||||
// }
|
||||
// }
|
||||
// return s + "]"
|
||||
// }
|
||||
294
event/feed_test.go
Normal file
294
event/feed_test.go
Normal file
@@ -0,0 +1,294 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFeedPanics(t *testing.T) {
|
||||
{
|
||||
var f Feed
|
||||
f.Send(int(2))
|
||||
want := feedTypeError{op: "Send", got: reflect.TypeOf(uint64(0)), want: reflect.TypeOf(int(0))}
|
||||
if err := checkPanic(want, func() { f.Send(uint64(2)) }); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
{
|
||||
var f Feed
|
||||
ch := make(chan int)
|
||||
f.Subscribe(ch)
|
||||
want := feedTypeError{op: "Send", got: reflect.TypeOf(uint64(0)), want: reflect.TypeOf(int(0))}
|
||||
if err := checkPanic(want, func() { f.Send(uint64(2)) }); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
{
|
||||
var f Feed
|
||||
f.Send(int(2))
|
||||
want := feedTypeError{op: "Subscribe", got: reflect.TypeOf(make(chan uint64)), want: reflect.TypeOf(make(chan<- int))}
|
||||
if err := checkPanic(want, func() { f.Subscribe(make(chan uint64)) }); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
{
|
||||
var f Feed
|
||||
if err := checkPanic(errBadChannel, func() { f.Subscribe(make(<-chan int)) }); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
{
|
||||
var f Feed
|
||||
if err := checkPanic(errBadChannel, func() { f.Subscribe(int(0)) }); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkPanic(want error, fn func()) (err error) {
|
||||
defer func() {
|
||||
panic := recover()
|
||||
if panic == nil {
|
||||
err = fmt.Errorf("didn't panic")
|
||||
} else if !reflect.DeepEqual(panic, want) {
|
||||
err = fmt.Errorf("panicked with wrong error: got %q, want %q", panic, want)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestFeed(t *testing.T) {
|
||||
var feed Feed
|
||||
var done, subscribed sync.WaitGroup
|
||||
subscriber := func(i int) {
|
||||
defer done.Done()
|
||||
|
||||
subchan := make(chan int)
|
||||
sub := feed.Subscribe(subchan)
|
||||
timeout := time.NewTimer(2 * time.Second)
|
||||
subscribed.Done()
|
||||
|
||||
select {
|
||||
case v := <-subchan:
|
||||
if v != 1 {
|
||||
t.Errorf("%d: received value %d, want 1", i, v)
|
||||
}
|
||||
case <-timeout.C:
|
||||
t.Errorf("%d: receive timeout", i)
|
||||
}
|
||||
|
||||
sub.Unsubscribe()
|
||||
select {
|
||||
case _, ok := <-sub.Err():
|
||||
if ok {
|
||||
t.Errorf("%d: error channel not closed after unsubscribe", i)
|
||||
}
|
||||
case <-timeout.C:
|
||||
t.Errorf("%d: unsubscribe timeout", i)
|
||||
}
|
||||
}
|
||||
|
||||
const n = 1000
|
||||
done.Add(n)
|
||||
subscribed.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go subscriber(i)
|
||||
}
|
||||
subscribed.Wait()
|
||||
if nsent := feed.Send(1); nsent != n {
|
||||
t.Errorf("first send delivered %d times, want %d", nsent, n)
|
||||
}
|
||||
if nsent := feed.Send(2); nsent != 0 {
|
||||
t.Errorf("second send delivered %d times, want 0", nsent)
|
||||
}
|
||||
done.Wait()
|
||||
}
|
||||
|
||||
func TestFeedSubscribeSameChannel(t *testing.T) {
|
||||
var (
|
||||
feed Feed
|
||||
done sync.WaitGroup
|
||||
ch = make(chan int)
|
||||
sub1 = feed.Subscribe(ch)
|
||||
sub2 = feed.Subscribe(ch)
|
||||
_ = feed.Subscribe(ch)
|
||||
)
|
||||
expectSends := func(value, n int) {
|
||||
if nsent := feed.Send(value); nsent != n {
|
||||
t.Errorf("send delivered %d times, want %d", nsent, n)
|
||||
}
|
||||
done.Done()
|
||||
}
|
||||
expectRecv := func(wantValue, n int) {
|
||||
for i := 0; i < n; i++ {
|
||||
if v := <-ch; v != wantValue {
|
||||
t.Errorf("received %d, want %d", v, wantValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done.Add(1)
|
||||
go expectSends(1, 3)
|
||||
expectRecv(1, 3)
|
||||
done.Wait()
|
||||
|
||||
sub1.Unsubscribe()
|
||||
|
||||
done.Add(1)
|
||||
go expectSends(2, 2)
|
||||
expectRecv(2, 2)
|
||||
done.Wait()
|
||||
|
||||
sub2.Unsubscribe()
|
||||
|
||||
done.Add(1)
|
||||
go expectSends(3, 1)
|
||||
expectRecv(3, 1)
|
||||
done.Wait()
|
||||
}
|
||||
|
||||
func TestFeedSubscribeBlockedPost(t *testing.T) {
|
||||
var (
|
||||
feed Feed
|
||||
nsends = 2000
|
||||
ch1 = make(chan int)
|
||||
ch2 = make(chan int)
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
defer wg.Wait()
|
||||
|
||||
feed.Subscribe(ch1)
|
||||
wg.Add(nsends)
|
||||
for i := 0; i < nsends; i++ {
|
||||
go func() {
|
||||
feed.Send(99)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
sub2 := feed.Subscribe(ch2)
|
||||
defer sub2.Unsubscribe()
|
||||
|
||||
// We're done when ch1 has received N times.
|
||||
// The number of receives on ch2 depends on scheduling.
|
||||
for i := 0; i < nsends; {
|
||||
select {
|
||||
case <-ch1:
|
||||
i++
|
||||
case <-ch2:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedUnsubscribeBlockedPost(t *testing.T) {
|
||||
var (
|
||||
feed Feed
|
||||
nsends = 200
|
||||
chans = make([]chan int, 2000)
|
||||
subs = make([]Subscription, len(chans))
|
||||
bchan = make(chan int)
|
||||
bsub = feed.Subscribe(bchan)
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
for i := range chans {
|
||||
chans[i] = make(chan int, nsends)
|
||||
}
|
||||
|
||||
// Queue up some Sends. None of these can make progress while bchan isn't read.
|
||||
wg.Add(nsends)
|
||||
for i := 0; i < nsends; i++ {
|
||||
go func() {
|
||||
feed.Send(99)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
// Subscribe the other channels.
|
||||
for i, ch := range chans {
|
||||
subs[i] = feed.Subscribe(ch)
|
||||
}
|
||||
// Unsubscribe them again.
|
||||
for _, sub := range subs {
|
||||
sub.Unsubscribe()
|
||||
}
|
||||
// Unblock the Sends.
|
||||
bsub.Unsubscribe()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestFeedUnsubscribeFromInbox(t *testing.T) {
|
||||
var (
|
||||
feed Feed
|
||||
ch1 = make(chan int)
|
||||
ch2 = make(chan int)
|
||||
sub1 = feed.Subscribe(ch1)
|
||||
sub2 = feed.Subscribe(ch1)
|
||||
sub3 = feed.Subscribe(ch2)
|
||||
)
|
||||
if len(feed.inbox) != 3 {
|
||||
t.Errorf("inbox length != 3 after subscribe")
|
||||
}
|
||||
if len(feed.sendCases) != 1 {
|
||||
t.Errorf("sendCases is non-empty after unsubscribe")
|
||||
}
|
||||
|
||||
sub1.Unsubscribe()
|
||||
sub2.Unsubscribe()
|
||||
sub3.Unsubscribe()
|
||||
if len(feed.inbox) != 0 {
|
||||
t.Errorf("inbox is non-empty after unsubscribe")
|
||||
}
|
||||
if len(feed.sendCases) != 1 {
|
||||
t.Errorf("sendCases is non-empty after unsubscribe")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFeedSend1000(b *testing.B) {
|
||||
var (
|
||||
done sync.WaitGroup
|
||||
feed Feed
|
||||
nsubs = 1000
|
||||
)
|
||||
subscriber := func(ch <-chan int) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
<-ch
|
||||
}
|
||||
done.Done()
|
||||
}
|
||||
done.Add(nsubs)
|
||||
for i := 0; i < nsubs; i++ {
|
||||
ch := make(chan int, 200)
|
||||
feed.Subscribe(ch)
|
||||
go subscriber(ch)
|
||||
}
|
||||
|
||||
// The actual benchmark.
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if feed.Send(i) != nsubs {
|
||||
panic("wrong number of sends")
|
||||
}
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
done.Wait()
|
||||
}
|
||||
275
event/subscription.go
Normal file
275
event/subscription.go
Normal file
@@ -0,0 +1,275 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Subscription represents a stream of events. The carrier of the events is typically a
|
||||
// channel, but isn't part of the interface.
|
||||
//
|
||||
// Subscriptions can fail while established. Failures are reported through an error
|
||||
// channel. It receives a value if there is an issue with the subscription (e.g. the
|
||||
// network connection delivering the events has been closed). Only one value will ever be
|
||||
// sent.
|
||||
//
|
||||
// The error channel is closed when the subscription ends successfully (i.e. when the
|
||||
// source of events is closed). It is also closed when Unsubscribe is called.
|
||||
//
|
||||
// The Unsubscribe method cancels the sending of events. You must call Unsubscribe in all
|
||||
// cases to ensure that resources related to the subscription are released. It can be
|
||||
// called any number of times.
|
||||
type Subscription interface {
|
||||
Err() <-chan error // returns the error channel
|
||||
Unsubscribe() // cancels sending of events, closing the error channel
|
||||
}
|
||||
|
||||
// NewSubscription runs a producer function as a subscription in a new goroutine. The
|
||||
// channel given to the producer is closed when Unsubscribe is called. If fn returns an
|
||||
// error, it is sent on the subscription's error channel.
|
||||
func NewSubscription(producer func(<-chan struct{}) error) Subscription {
|
||||
s := &funcSub{unsub: make(chan struct{}), err: make(chan error, 1)}
|
||||
go func() {
|
||||
defer close(s.err)
|
||||
err := producer(s.unsub)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if !s.unsubscribed {
|
||||
if err != nil {
|
||||
s.err <- err
|
||||
}
|
||||
s.unsubscribed = true
|
||||
}
|
||||
}()
|
||||
return s
|
||||
}
|
||||
|
||||
type funcSub struct {
|
||||
unsub chan struct{}
|
||||
err chan error
|
||||
mu sync.Mutex
|
||||
unsubscribed bool
|
||||
}
|
||||
|
||||
func (s *funcSub) Unsubscribe() {
|
||||
s.mu.Lock()
|
||||
if s.unsubscribed {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.unsubscribed = true
|
||||
close(s.unsub)
|
||||
s.mu.Unlock()
|
||||
// Wait for producer shutdown.
|
||||
<-s.err
|
||||
}
|
||||
|
||||
func (s *funcSub) Err() <-chan error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
// Resubscribe calls fn repeatedly to keep a subscription established. When the
|
||||
// subscription is established, Resubscribe waits for it to fail and calls fn again. This
|
||||
// process repeats until Unsubscribe is called or the active subscription ends
|
||||
// successfully.
|
||||
//
|
||||
// Resubscribe applies backoff between calls to fn. The time between calls is adapted
|
||||
// based on the error rate, but will never exceed backoffMax.
|
||||
func Resubscribe(backoffMax time.Duration, fn ResubscribeFunc) Subscription {
|
||||
s := &resubscribeSub{
|
||||
waitTime: backoffMax / 10,
|
||||
backoffMax: backoffMax,
|
||||
fn: fn,
|
||||
err: make(chan error),
|
||||
unsub: make(chan struct{}),
|
||||
}
|
||||
go s.loop()
|
||||
return s
|
||||
}
|
||||
|
||||
// A ResubscribeFunc attempts to establish a subscription.
|
||||
type ResubscribeFunc func(context.Context) (Subscription, error)
|
||||
|
||||
type resubscribeSub struct {
|
||||
fn ResubscribeFunc
|
||||
err chan error
|
||||
unsub chan struct{}
|
||||
unsubOnce sync.Once
|
||||
lastTry mclock.AbsTime
|
||||
waitTime, backoffMax time.Duration
|
||||
}
|
||||
|
||||
func (s *resubscribeSub) Unsubscribe() {
|
||||
s.unsubOnce.Do(func() {
|
||||
s.unsub <- struct{}{}
|
||||
<-s.err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *resubscribeSub) Err() <-chan error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s *resubscribeSub) loop() {
|
||||
defer close(s.err)
|
||||
var done bool
|
||||
for !done {
|
||||
sub := s.subscribe()
|
||||
if sub == nil {
|
||||
break
|
||||
}
|
||||
done = s.waitForError(sub)
|
||||
sub.Unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *resubscribeSub) subscribe() Subscription {
|
||||
subscribed := make(chan error)
|
||||
var sub Subscription
|
||||
retry:
|
||||
for {
|
||||
s.lastTry = mclock.Now()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
rsub, err := s.fn(ctx)
|
||||
sub = rsub
|
||||
subscribed <- err
|
||||
}()
|
||||
select {
|
||||
case err := <-subscribed:
|
||||
cancel()
|
||||
if err != nil {
|
||||
// Subscribing failed, wait before launching the next try.
|
||||
if s.backoffWait() {
|
||||
return nil
|
||||
}
|
||||
continue retry
|
||||
}
|
||||
if sub == nil {
|
||||
panic("event: ResubscribeFunc returned nil subscription and no error")
|
||||
}
|
||||
return sub
|
||||
case <-s.unsub:
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *resubscribeSub) waitForError(sub Subscription) bool {
|
||||
defer sub.Unsubscribe()
|
||||
select {
|
||||
case err := <-sub.Err():
|
||||
return err == nil
|
||||
case <-s.unsub:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *resubscribeSub) backoffWait() bool {
|
||||
if time.Duration(mclock.Now()-s.lastTry) > s.backoffMax {
|
||||
s.waitTime = s.backoffMax / 10
|
||||
} else {
|
||||
s.waitTime *= 2
|
||||
if s.waitTime > s.backoffMax {
|
||||
s.waitTime = s.backoffMax
|
||||
}
|
||||
}
|
||||
|
||||
t := time.NewTimer(s.waitTime)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case <-t.C:
|
||||
return false
|
||||
case <-s.unsub:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// SubscriptionScope provides a facility to unsubscribe multiple subscriptions at once.
|
||||
//
|
||||
// For code that handle more than one subscription, a scope can be used to conveniently
|
||||
// unsubscribe all of them with a single call. The example demonstrates a typical use in a
|
||||
// larger program.
|
||||
//
|
||||
// The zero value is ready to use.
|
||||
type SubscriptionScope struct {
|
||||
mu sync.Mutex
|
||||
subs map[*scopeSub]struct{}
|
||||
closed bool
|
||||
}
|
||||
|
||||
type scopeSub struct {
|
||||
sc *SubscriptionScope
|
||||
s Subscription
|
||||
}
|
||||
|
||||
// Track starts tracking a subscription. If the scope is closed, Track returns nil. The
|
||||
// returned subscription is a wrapper. Unsubscribing the wrapper removes it from the
|
||||
// scope.
|
||||
func (sc *SubscriptionScope) Track(s Subscription) Subscription {
|
||||
sc.mu.Lock()
|
||||
defer sc.mu.Unlock()
|
||||
if sc.closed {
|
||||
return nil
|
||||
}
|
||||
if sc.subs == nil {
|
||||
sc.subs = make(map[*scopeSub]struct{})
|
||||
}
|
||||
ss := &scopeSub{sc, s}
|
||||
sc.subs[ss] = struct{}{}
|
||||
return ss
|
||||
}
|
||||
|
||||
// Close calls Unsubscribe on all tracked subscriptions and prevents further additions to
|
||||
// the tracked set. Calls to Track after Close return nil.
|
||||
func (sc *SubscriptionScope) Close() {
|
||||
sc.mu.Lock()
|
||||
defer sc.mu.Unlock()
|
||||
if sc.closed {
|
||||
return
|
||||
}
|
||||
sc.closed = true
|
||||
for s := range sc.subs {
|
||||
s.s.Unsubscribe()
|
||||
}
|
||||
sc.subs = nil
|
||||
}
|
||||
|
||||
// Count returns the number of tracked subscriptions.
|
||||
// It is meant to be used for debugging.
|
||||
func (sc *SubscriptionScope) Count() int {
|
||||
sc.mu.Lock()
|
||||
defer sc.mu.Unlock()
|
||||
return len(sc.subs)
|
||||
}
|
||||
|
||||
func (s *scopeSub) Unsubscribe() {
|
||||
s.s.Unsubscribe()
|
||||
s.sc.mu.Lock()
|
||||
defer s.sc.mu.Unlock()
|
||||
delete(s.sc.subs, s)
|
||||
}
|
||||
|
||||
func (s *scopeSub) Err() <-chan error {
|
||||
return s.s.Err()
|
||||
}
|
||||
121
event/subscription_test.go
Normal file
121
event/subscription_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var errInts = errors.New("error in subscribeInts")
|
||||
|
||||
func subscribeInts(max, fail int, c chan<- int) Subscription {
|
||||
return NewSubscription(func(quit <-chan struct{}) error {
|
||||
for i := 0; i < max; i++ {
|
||||
if i >= fail {
|
||||
return errInts
|
||||
}
|
||||
select {
|
||||
case c <- i:
|
||||
case <-quit:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewSubscriptionError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
channel := make(chan int)
|
||||
sub := subscribeInts(10, 2, channel)
|
||||
loop:
|
||||
for want := 0; want < 10; want++ {
|
||||
select {
|
||||
case got := <-channel:
|
||||
if got != want {
|
||||
t.Fatalf("wrong int %d, want %d", got, want)
|
||||
}
|
||||
case err := <-sub.Err():
|
||||
if err != errInts {
|
||||
t.Fatalf("wrong error: got %q, want %q", err, errInts)
|
||||
}
|
||||
if want != 2 {
|
||||
t.Fatalf("got errInts at int %d, should be received at 2", want)
|
||||
}
|
||||
break loop
|
||||
}
|
||||
}
|
||||
sub.Unsubscribe()
|
||||
|
||||
err, ok := <-sub.Err()
|
||||
if err != nil {
|
||||
t.Fatal("got non-nil error after Unsubscribe")
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("channel still open after Unsubscribe")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResubscribe(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var i int
|
||||
nfails := 6
|
||||
sub := Resubscribe(100*time.Millisecond, func(ctx context.Context) (Subscription, error) {
|
||||
// fmt.Printf("call #%d @ %v\n", i, time.Now())
|
||||
i++
|
||||
if i == 2 {
|
||||
// Delay the second failure a bit to reset the resubscribe interval.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
if i < nfails {
|
||||
return nil, errors.New("oops")
|
||||
}
|
||||
sub := NewSubscription(func(unsubscribed <-chan struct{}) error { return nil })
|
||||
return sub, nil
|
||||
})
|
||||
|
||||
<-sub.Err()
|
||||
if i != nfails {
|
||||
t.Fatalf("resubscribe function called %d times, want %d times", i, nfails)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResubscribeAbort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
done := make(chan error)
|
||||
sub := Resubscribe(0, func(ctx context.Context) (Subscription, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
done <- nil
|
||||
case <-time.After(2 * time.Second):
|
||||
done <- errors.New("context given to resubscribe function not canceled within 2s")
|
||||
}
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
sub.Unsubscribe()
|
||||
if err := <-done; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
|
||||
"github.com/ethereum/ethash"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
@@ -185,8 +186,14 @@ func NewPublicAccountAPI(am *accounts.Manager) *PublicAccountAPI {
|
||||
}
|
||||
|
||||
// Accounts returns the collection of accounts this node manages
|
||||
func (s *PublicAccountAPI) Accounts() []accounts.Account {
|
||||
return s.am.Accounts()
|
||||
func (s *PublicAccountAPI) Accounts() []common.Address {
|
||||
var addresses []common.Address
|
||||
for _, wallet := range s.am.Wallets() {
|
||||
for _, account := range wallet.Accounts() {
|
||||
addresses = append(addresses, account.Address)
|
||||
}
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
// PrivateAccountAPI provides an API to access accounts managed by this node.
|
||||
@@ -207,23 +214,67 @@ func NewPrivateAccountAPI(b Backend) *PrivateAccountAPI {
|
||||
|
||||
// ListAccounts will return a list of addresses for accounts this node manages.
|
||||
func (s *PrivateAccountAPI) ListAccounts() []common.Address {
|
||||
accounts := s.am.Accounts()
|
||||
addresses := make([]common.Address, len(accounts))
|
||||
for i, acc := range accounts {
|
||||
addresses[i] = acc.Address
|
||||
var addresses []common.Address
|
||||
for _, wallet := range s.am.Wallets() {
|
||||
for _, account := range wallet.Accounts() {
|
||||
addresses = append(addresses, account.Address)
|
||||
}
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
// rawWallet is a JSON representation of an accounts.Wallet interface, with its
|
||||
// data contents extracted into plain fields.
|
||||
type rawWallet struct {
|
||||
URL string `json:"url"`
|
||||
Status string `json:"status"`
|
||||
Accounts []accounts.Account `json:"accounts"`
|
||||
}
|
||||
|
||||
// ListWallets will return a list of wallets this node manages.
|
||||
func (s *PrivateAccountAPI) ListWallets() []rawWallet {
|
||||
var wallets []rawWallet
|
||||
for _, wallet := range s.am.Wallets() {
|
||||
wallets = append(wallets, rawWallet{
|
||||
URL: wallet.URL().String(),
|
||||
Status: wallet.Status(),
|
||||
Accounts: wallet.Accounts(),
|
||||
})
|
||||
}
|
||||
return wallets
|
||||
}
|
||||
|
||||
// DeriveAccount requests a HD wallet to derive a new account, optionally pinning
|
||||
// it for later reuse.
|
||||
func (s *PrivateAccountAPI) DeriveAccount(url string, path string, pin *bool) (accounts.Account, error) {
|
||||
wallet, err := s.am.Wallet(url)
|
||||
if err != nil {
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
derivPath, err := accounts.ParseDerivationPath(path)
|
||||
if err != nil {
|
||||
return accounts.Account{}, err
|
||||
}
|
||||
if pin == nil {
|
||||
pin = new(bool)
|
||||
}
|
||||
return wallet.Derive(derivPath, *pin)
|
||||
}
|
||||
|
||||
// NewAccount will create a new account and returns the address for the new account.
|
||||
func (s *PrivateAccountAPI) NewAccount(password string) (common.Address, error) {
|
||||
acc, err := s.am.NewAccount(password)
|
||||
acc, err := fetchKeystore(s.am).NewAccount(password)
|
||||
if err == nil {
|
||||
return acc.Address, nil
|
||||
}
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
// fetchKeystore retrives the encrypted keystore from the account manager.
|
||||
func fetchKeystore(am *accounts.Manager) *keystore.KeyStore {
|
||||
return am.Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
}
|
||||
|
||||
// ImportRawKey stores the given hex encoded ECDSA key into the key directory,
|
||||
// encrypting it with the passphrase.
|
||||
func (s *PrivateAccountAPI) ImportRawKey(privkey string, password string) (common.Address, error) {
|
||||
@@ -232,7 +283,7 @@ func (s *PrivateAccountAPI) ImportRawKey(privkey string, password string) (commo
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
acc, err := s.am.ImportECDSA(crypto.ToECDSA(hexkey), password)
|
||||
acc, err := fetchKeystore(s.am).ImportECDSA(crypto.ToECDSA(hexkey), password)
|
||||
return acc.Address, err
|
||||
}
|
||||
|
||||
@@ -249,30 +300,42 @@ func (s *PrivateAccountAPI) UnlockAccount(addr common.Address, password string,
|
||||
} else {
|
||||
d = time.Duration(*duration) * time.Second
|
||||
}
|
||||
err := s.am.TimedUnlock(accounts.Account{Address: addr}, password, d)
|
||||
err := fetchKeystore(s.am).TimedUnlock(accounts.Account{Address: addr}, password, d)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
// LockAccount will lock the account associated with the given address when it's unlocked.
|
||||
func (s *PrivateAccountAPI) LockAccount(addr common.Address) bool {
|
||||
return s.am.Lock(addr) == nil
|
||||
return fetchKeystore(s.am).Lock(addr) == nil
|
||||
}
|
||||
|
||||
// SendTransaction will create a transaction from the given arguments and
|
||||
// tries to sign it with the key associated with args.To. If the given passwd isn't
|
||||
// able to decrypt the key it fails.
|
||||
func (s *PrivateAccountAPI) SendTransaction(ctx context.Context, args SendTxArgs, passwd string) (common.Hash, error) {
|
||||
// Set some sanity defaults and terminate on failure
|
||||
if err := args.setDefaults(ctx, s.b); err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
tx := args.toTransaction()
|
||||
signer := types.MakeSigner(s.b.ChainConfig(), s.b.CurrentBlock().Number())
|
||||
signature, err := s.am.SignWithPassphrase(accounts.Account{Address: args.From}, passwd, signer.Hash(tx).Bytes())
|
||||
// Look up the wallet containing the requested signer
|
||||
account := accounts.Account{Address: args.From}
|
||||
|
||||
wallet, err := s.am.Find(account)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
// Assemble the transaction and sign with the wallet
|
||||
tx := args.toTransaction()
|
||||
|
||||
return submitTransaction(ctx, s.b, tx, signature)
|
||||
var chainID *big.Int
|
||||
if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) {
|
||||
chainID = config.ChainId
|
||||
}
|
||||
signed, err := wallet.SignTxWithPassphrase(account, passwd, tx, chainID)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
return submitTransaction(ctx, s.b, signed)
|
||||
}
|
||||
|
||||
// signHash is a helper function that calculates a hash for the given message that can be
|
||||
@@ -297,7 +360,15 @@ func signHash(data []byte) []byte {
|
||||
//
|
||||
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign
|
||||
func (s *PrivateAccountAPI) Sign(ctx context.Context, data hexutil.Bytes, addr common.Address, passwd string) (hexutil.Bytes, error) {
|
||||
signature, err := s.b.AccountManager().SignWithPassphrase(accounts.Account{Address: addr}, passwd, signHash(data))
|
||||
// Look up the wallet containing the requested signer
|
||||
account := accounts.Account{Address: addr}
|
||||
|
||||
wallet, err := s.b.AccountManager().Find(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Assemble sign the data with the wallet
|
||||
signature, err := wallet.SignHashWithPassphrase(account, passwd, signHash(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -510,21 +581,18 @@ func (s *PublicBlockChainAPI) doCall(ctx context.Context, args CallArgs, blockNr
|
||||
if state == nil || err != nil {
|
||||
return "0x", common.Big0, err
|
||||
}
|
||||
|
||||
// Set the account address to interact with
|
||||
var addr common.Address
|
||||
if args.From == (common.Address{}) {
|
||||
accounts := s.b.AccountManager().Accounts()
|
||||
if len(accounts) == 0 {
|
||||
addr = common.Address{}
|
||||
} else {
|
||||
addr = accounts[0].Address
|
||||
// Set sender address or use a default if none specified
|
||||
addr := args.From
|
||||
if addr == (common.Address{}) {
|
||||
if wallets := s.b.AccountManager().Wallets(); len(wallets) > 0 {
|
||||
if accounts := wallets[0].Accounts(); len(accounts) > 0 {
|
||||
addr = accounts[0].Address
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addr = args.From
|
||||
}
|
||||
|
||||
// Assemble the CALL invocation
|
||||
// Set default gas & gas price if none were set
|
||||
gas, gasPrice := args.Gas.ToInt(), args.GasPrice.ToInt()
|
||||
if gas.Cmp(common.Big0) == 0 {
|
||||
gas = big.NewInt(50000000)
|
||||
@@ -1003,13 +1071,19 @@ func (s *PublicTransactionPoolAPI) GetTransactionReceipt(txHash common.Hash) (ma
|
||||
|
||||
// sign is a helper function that signs a transaction with the private key of the given address.
|
||||
func (s *PublicTransactionPoolAPI) sign(addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
|
||||
signer := types.MakeSigner(s.b.ChainConfig(), s.b.CurrentBlock().Number())
|
||||
// Look up the wallet containing the requested signer
|
||||
account := accounts.Account{Address: addr}
|
||||
|
||||
signature, err := s.b.AccountManager().Sign(addr, signer.Hash(tx).Bytes())
|
||||
wallet, err := s.b.AccountManager().Find(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tx.WithSignature(signer, signature)
|
||||
// Request the wallet to sign the transaction
|
||||
var chainID *big.Int
|
||||
if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) {
|
||||
chainID = config.ChainId
|
||||
}
|
||||
return wallet.SignTx(account, tx, chainID)
|
||||
}
|
||||
|
||||
// SendTxArgs represents the arguments to sumbit a new transaction into the transaction pool.
|
||||
@@ -1056,42 +1130,47 @@ func (args *SendTxArgs) toTransaction() *types.Transaction {
|
||||
}
|
||||
|
||||
// submitTransaction is a helper function that submits tx to txPool and logs a message.
|
||||
func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction, signature []byte) (common.Hash, error) {
|
||||
signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number())
|
||||
|
||||
signedTx, err := tx.WithSignature(signer, signature)
|
||||
if err != nil {
|
||||
func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) {
|
||||
if err := b.SendTx(ctx, tx); err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
if err := b.SendTx(ctx, signedTx); err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
if signedTx.To() == nil {
|
||||
from, _ := types.Sender(signer, signedTx)
|
||||
addr := crypto.CreateAddress(from, signedTx.Nonce())
|
||||
glog.V(logger.Info).Infof("Tx(%s) created: %s\n", signedTx.Hash().Hex(), addr.Hex())
|
||||
if tx.To() == nil {
|
||||
signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number())
|
||||
from, _ := types.Sender(signer, tx)
|
||||
addr := crypto.CreateAddress(from, tx.Nonce())
|
||||
glog.V(logger.Info).Infof("Tx(%s) created: %s\n", tx.Hash().Hex(), addr.Hex())
|
||||
} else {
|
||||
glog.V(logger.Info).Infof("Tx(%s) to: %s\n", signedTx.Hash().Hex(), tx.To().Hex())
|
||||
glog.V(logger.Info).Infof("Tx(%s) to: %s\n", tx.Hash().Hex(), tx.To().Hex())
|
||||
}
|
||||
|
||||
return signedTx.Hash(), nil
|
||||
return tx.Hash(), nil
|
||||
}
|
||||
|
||||
// SendTransaction creates a transaction for the given argument, sign it and submit it to the
|
||||
// transaction pool.
|
||||
func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) {
|
||||
// Set some sanity defaults and terminate on failure
|
||||
if err := args.setDefaults(ctx, s.b); err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
tx := args.toTransaction()
|
||||
signer := types.MakeSigner(s.b.ChainConfig(), s.b.CurrentBlock().Number())
|
||||
signature, err := s.b.AccountManager().Sign(args.From, signer.Hash(tx).Bytes())
|
||||
// Look up the wallet containing the requested signer
|
||||
account := accounts.Account{Address: args.From}
|
||||
|
||||
wallet, err := s.b.AccountManager().Find(account)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
return submitTransaction(ctx, s.b, tx, signature)
|
||||
// Assemble the transaction and sign with the wallet
|
||||
tx := args.toTransaction()
|
||||
|
||||
var chainID *big.Int
|
||||
if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) {
|
||||
chainID = config.ChainId
|
||||
}
|
||||
signed, err := wallet.SignTx(account, tx, chainID)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
return submitTransaction(ctx, s.b, signed)
|
||||
}
|
||||
|
||||
// SendRawTransaction will add the signed transaction to the transaction pool.
|
||||
@@ -1131,7 +1210,15 @@ func (s *PublicTransactionPoolAPI) SendRawTransaction(ctx context.Context, encod
|
||||
//
|
||||
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign
|
||||
func (s *PublicTransactionPoolAPI) Sign(addr common.Address, data hexutil.Bytes) (hexutil.Bytes, error) {
|
||||
signature, err := s.b.AccountManager().Sign(addr, signHash(data))
|
||||
// Look up the wallet containing the requested signer
|
||||
account := accounts.Account{Address: addr}
|
||||
|
||||
wallet, err := s.b.AccountManager().Find(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Sign the requested hash with the wallet
|
||||
signature, err := wallet.SignHash(account, signHash(data))
|
||||
if err == nil {
|
||||
signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
|
||||
}
|
||||
@@ -1177,7 +1264,7 @@ func (s *PublicTransactionPoolAPI) PendingTransactions() ([]*RPCTransaction, err
|
||||
signer = types.NewEIP155Signer(tx.ChainId())
|
||||
}
|
||||
from, _ := types.Sender(signer, tx)
|
||||
if s.b.AccountManager().HasAddress(from) {
|
||||
if _, err := s.b.AccountManager().Find(accounts.Account{Address: from}); err == nil {
|
||||
transactions = append(transactions, newRPCPendingTransaction(tx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,14 @@ package guide
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
// Tests that the account management snippets work correctly.
|
||||
@@ -42,59 +43,59 @@ func TestAccountManagement(t *testing.T) {
|
||||
}
|
||||
defer os.RemoveAll(workdir)
|
||||
|
||||
// Create an encrypted keystore manager with standard crypto parameters
|
||||
am := accounts.NewManager(filepath.Join(workdir, "keystore"), accounts.StandardScryptN, accounts.StandardScryptP)
|
||||
// Create an encrypted keystore with standard crypto parameters
|
||||
ks := keystore.NewKeyStore(filepath.Join(workdir, "keystore"), keystore.StandardScryptN, keystore.StandardScryptP)
|
||||
|
||||
// Create a new account with the specified encryption passphrase
|
||||
newAcc, err := am.NewAccount("Creation password")
|
||||
newAcc, err := ks.NewAccount("Creation password")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new account: %v", err)
|
||||
}
|
||||
// Export the newly created account with a different passphrase. The returned
|
||||
// data from this method invocation is a JSON encoded, encrypted key-file
|
||||
jsonAcc, err := am.Export(newAcc, "Creation password", "Export password")
|
||||
jsonAcc, err := ks.Export(newAcc, "Creation password", "Export password")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to export account: %v", err)
|
||||
}
|
||||
// Update the passphrase on the account created above inside the local keystore
|
||||
if err := am.Update(newAcc, "Creation password", "Update password"); err != nil {
|
||||
if err := ks.Update(newAcc, "Creation password", "Update password"); err != nil {
|
||||
t.Fatalf("Failed to update account: %v", err)
|
||||
}
|
||||
// Delete the account updated above from the local keystore
|
||||
if err := am.Delete(newAcc, "Update password"); err != nil {
|
||||
if err := ks.Delete(newAcc, "Update password"); err != nil {
|
||||
t.Fatalf("Failed to delete account: %v", err)
|
||||
}
|
||||
// Import back the account we've exported (and then deleted) above with yet
|
||||
// again a fresh passphrase
|
||||
if _, err := am.Import(jsonAcc, "Export password", "Import password"); err != nil {
|
||||
if _, err := ks.Import(jsonAcc, "Export password", "Import password"); err != nil {
|
||||
t.Fatalf("Failed to import account: %v", err)
|
||||
}
|
||||
// Create a new account to sign transactions with
|
||||
signer, err := am.NewAccount("Signer password")
|
||||
signer, err := ks.NewAccount("Signer password")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create signer account: %v", err)
|
||||
}
|
||||
txHash := common.HexToHash("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
tx, chain := new(types.Transaction), big.NewInt(1)
|
||||
|
||||
// Sign a transaction with a single authorization
|
||||
if _, err := am.SignWithPassphrase(signer, "Signer password", txHash.Bytes()); err != nil {
|
||||
if _, err := ks.SignTxWithPassphrase(signer, "Signer password", tx, chain); err != nil {
|
||||
t.Fatalf("Failed to sign with passphrase: %v", err)
|
||||
}
|
||||
// Sign a transaction with multiple manually cancelled authorizations
|
||||
if err := am.Unlock(signer, "Signer password"); err != nil {
|
||||
if err := ks.Unlock(signer, "Signer password"); err != nil {
|
||||
t.Fatalf("Failed to unlock account: %v", err)
|
||||
}
|
||||
if _, err := am.Sign(signer.Address, txHash.Bytes()); err != nil {
|
||||
if _, err := ks.SignTx(signer, tx, chain); err != nil {
|
||||
t.Fatalf("Failed to sign with unlocked account: %v", err)
|
||||
}
|
||||
if err := am.Lock(signer.Address); err != nil {
|
||||
if err := ks.Lock(signer.Address); err != nil {
|
||||
t.Fatalf("Failed to lock account: %v", err)
|
||||
}
|
||||
// Sign a transaction with multiple automatically cancelled authorizations
|
||||
if err := am.TimedUnlock(signer, "Signer password", time.Second); err != nil {
|
||||
if err := ks.TimedUnlock(signer, "Signer password", time.Second); err != nil {
|
||||
t.Fatalf("Failed to time unlock account: %v", err)
|
||||
}
|
||||
if _, err := am.Sign(signer.Address, txHash.Bytes()); err != nil {
|
||||
if _, err := ks.SignTx(signer, tx, chain); err != nil {
|
||||
t.Fatalf("Failed to sign with time unlocked account: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,8 +206,8 @@ type bintree struct {
|
||||
}
|
||||
|
||||
var _bintree = &bintree{nil, map[string]*bintree{
|
||||
"bignumber.js": &bintree{bignumberJs, map[string]*bintree{}},
|
||||
"web3.js": &bintree{web3Js, map[string]*bintree{}},
|
||||
"bignumber.js": {bignumberJs, map[string]*bintree{}},
|
||||
"web3.js": {web3Js, map[string]*bintree{}},
|
||||
}}
|
||||
|
||||
// RestoreAsset restores an asset under the given directory
|
||||
|
||||
@@ -448,6 +448,18 @@ web3._extend({
|
||||
name: 'ecRecover',
|
||||
call: 'personal_ecRecover',
|
||||
params: 2
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'deriveAccount',
|
||||
call: 'personal_deriveAccount',
|
||||
params: 3
|
||||
})
|
||||
],
|
||||
properties:
|
||||
[
|
||||
new web3._extend.Property({
|
||||
name: 'listWallets',
|
||||
getter: 'personal_listWallets'
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ type TxPool struct {
|
||||
signer types.Signer
|
||||
quit chan bool
|
||||
eventMux *event.TypeMux
|
||||
events event.Subscription
|
||||
events *event.TypeMuxSubscription
|
||||
mu sync.RWMutex
|
||||
chain *LightChain
|
||||
odr OdrBackend
|
||||
|
||||
@@ -90,7 +90,7 @@ type worker struct {
|
||||
|
||||
// update loop
|
||||
mux *event.TypeMux
|
||||
events event.Subscription
|
||||
events *event.TypeMuxSubscription
|
||||
wg sync.WaitGroup
|
||||
|
||||
agents map[Agent]struct{}
|
||||
@@ -386,8 +386,11 @@ func (self *worker) makeCurrent(parent *types.Block, header *types.Header) error
|
||||
work.family.Add(ancestor.Hash())
|
||||
work.ancestors.Add(ancestor.Hash())
|
||||
}
|
||||
accounts := self.eth.AccountManager().Accounts()
|
||||
|
||||
wallets := self.eth.AccountManager().Wallets()
|
||||
accounts := make([]accounts.Account, 0, len(wallets))
|
||||
for _, wallet := range wallets {
|
||||
accounts = append(accounts, wallet.Accounts()...)
|
||||
}
|
||||
// Keep track of transactions which return errors so they can be removed
|
||||
work.tcount = 0
|
||||
work.ownedAccounts = accountAddressesSet(accounts)
|
||||
|
||||
@@ -24,24 +24,25 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
)
|
||||
|
||||
const (
|
||||
// StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB
|
||||
// memory and taking approximately 1s CPU time on a modern processor.
|
||||
StandardScryptN = int(accounts.StandardScryptN)
|
||||
StandardScryptN = int(keystore.StandardScryptN)
|
||||
|
||||
// StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB
|
||||
// memory and taking approximately 1s CPU time on a modern processor.
|
||||
StandardScryptP = int(accounts.StandardScryptP)
|
||||
StandardScryptP = int(keystore.StandardScryptP)
|
||||
|
||||
// LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB
|
||||
// memory and taking approximately 100ms CPU time on a modern processor.
|
||||
LightScryptN = int(accounts.LightScryptN)
|
||||
LightScryptN = int(keystore.LightScryptN)
|
||||
|
||||
// LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB
|
||||
// memory and taking approximately 100ms CPU time on a modern processor.
|
||||
LightScryptP = int(accounts.LightScryptP)
|
||||
LightScryptP = int(keystore.LightScryptP)
|
||||
)
|
||||
|
||||
// Account represents a stored key.
|
||||
@@ -77,59 +78,75 @@ func (a *Account) GetAddress() *Address {
|
||||
return &Address{a.account.Address}
|
||||
}
|
||||
|
||||
// GetFile retrieves the path of the file containing the account key.
|
||||
func (a *Account) GetFile() string {
|
||||
return a.account.File
|
||||
// GetURL retrieves the canonical URL of the account.
|
||||
func (a *Account) GetURL() string {
|
||||
return a.account.URL.String()
|
||||
}
|
||||
|
||||
// AccountManager manages a key storage directory on disk.
|
||||
type AccountManager struct{ manager *accounts.Manager }
|
||||
// KeyStore manages a key storage directory on disk.
|
||||
type KeyStore struct{ keystore *keystore.KeyStore }
|
||||
|
||||
// NewAccountManager creates a manager for the given directory.
|
||||
func NewAccountManager(keydir string, scryptN, scryptP int) *AccountManager {
|
||||
return &AccountManager{manager: accounts.NewManager(keydir, scryptN, scryptP)}
|
||||
// NewKeyStore creates a keystore for the given directory.
|
||||
func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore {
|
||||
return &KeyStore{keystore: keystore.NewKeyStore(keydir, scryptN, scryptP)}
|
||||
}
|
||||
|
||||
// HasAddress reports whether a key with the given address is present.
|
||||
func (am *AccountManager) HasAddress(address *Address) bool {
|
||||
return am.manager.HasAddress(address.address)
|
||||
func (ks *KeyStore) HasAddress(address *Address) bool {
|
||||
return ks.keystore.HasAddress(address.address)
|
||||
}
|
||||
|
||||
// GetAccounts returns all key files present in the directory.
|
||||
func (am *AccountManager) GetAccounts() *Accounts {
|
||||
return &Accounts{am.manager.Accounts()}
|
||||
func (ks *KeyStore) GetAccounts() *Accounts {
|
||||
return &Accounts{ks.keystore.Accounts()}
|
||||
}
|
||||
|
||||
// DeleteAccount deletes the key matched by account if the passphrase is correct.
|
||||
// If a contains no filename, the address must match a unique key.
|
||||
func (am *AccountManager) DeleteAccount(account *Account, passphrase string) error {
|
||||
return am.manager.Delete(accounts.Account{
|
||||
Address: account.account.Address,
|
||||
File: account.account.File,
|
||||
}, passphrase)
|
||||
func (ks *KeyStore) DeleteAccount(account *Account, passphrase string) error {
|
||||
return ks.keystore.Delete(account.account, passphrase)
|
||||
}
|
||||
|
||||
// Sign calculates a ECDSA signature for the given hash. The produced signature
|
||||
// SignHash calculates a ECDSA signature for the given hash. The produced signature
|
||||
// is in the [R || S || V] format where V is 0 or 1.
|
||||
func (am *AccountManager) Sign(address *Address, hash []byte) (signature []byte, _ error) {
|
||||
return am.manager.Sign(address.address, hash)
|
||||
func (ks *KeyStore) SignHash(address *Address, hash []byte) (signature []byte, _ error) {
|
||||
return ks.keystore.SignHash(accounts.Account{Address: address.address}, hash)
|
||||
}
|
||||
|
||||
// SignPassphrase signs hash if the private key matching the given address can
|
||||
// SignTx signs the given transaction with the requested account.
|
||||
func (ks *KeyStore) SignTx(account *Account, tx *Transaction, chainID *BigInt) (*Transaction, error) {
|
||||
signed, err := ks.keystore.SignTx(account.account, tx.tx, chainID.bigint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Transaction{signed}, nil
|
||||
}
|
||||
|
||||
// SignHashPassphrase signs hash if the private key matching the given address can
|
||||
// be decrypted with the given passphrase. The produced signature is in the
|
||||
// [R || S || V] format where V is 0 or 1.
|
||||
func (am *AccountManager) SignPassphrase(account *Account, passphrase string, hash []byte) (signature []byte, _ error) {
|
||||
return am.manager.SignWithPassphrase(account.account, passphrase, hash)
|
||||
func (ks *KeyStore) SignHashPassphrase(account *Account, passphrase string, hash []byte) (signature []byte, _ error) {
|
||||
return ks.keystore.SignHashWithPassphrase(account.account, passphrase, hash)
|
||||
}
|
||||
|
||||
// SignTxPassphrase signs the transaction if the private key matching the
|
||||
// given address can be decrypted with the given passphrase.
|
||||
func (ks *KeyStore) SignTxPassphrase(account *Account, passphrase string, tx *Transaction, chainID *BigInt) (*Transaction, error) {
|
||||
signed, err := ks.keystore.SignTxWithPassphrase(account.account, passphrase, tx.tx, chainID.bigint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Transaction{signed}, nil
|
||||
}
|
||||
|
||||
// Unlock unlocks the given account indefinitely.
|
||||
func (am *AccountManager) Unlock(account *Account, passphrase string) error {
|
||||
return am.manager.TimedUnlock(account.account, passphrase, 0)
|
||||
func (ks *KeyStore) Unlock(account *Account, passphrase string) error {
|
||||
return ks.keystore.TimedUnlock(account.account, passphrase, 0)
|
||||
}
|
||||
|
||||
// Lock removes the private key with the given address from memory.
|
||||
func (am *AccountManager) Lock(address *Address) error {
|
||||
return am.manager.Lock(address.address)
|
||||
func (ks *KeyStore) Lock(address *Address) error {
|
||||
return ks.keystore.Lock(address.address)
|
||||
}
|
||||
|
||||
// TimedUnlock unlocks the given account with the passphrase. The account stays
|
||||
@@ -139,14 +156,14 @@ func (am *AccountManager) Lock(address *Address) error {
|
||||
// If the account address is already unlocked for a duration, TimedUnlock extends or
|
||||
// shortens the active unlock timeout. If the address was previously unlocked
|
||||
// indefinitely the timeout is not altered.
|
||||
func (am *AccountManager) TimedUnlock(account *Account, passphrase string, timeout int64) error {
|
||||
return am.manager.TimedUnlock(account.account, passphrase, time.Duration(timeout))
|
||||
func (ks *KeyStore) TimedUnlock(account *Account, passphrase string, timeout int64) error {
|
||||
return ks.keystore.TimedUnlock(account.account, passphrase, time.Duration(timeout))
|
||||
}
|
||||
|
||||
// NewAccount generates a new key and stores it into the key directory,
|
||||
// encrypting it with the passphrase.
|
||||
func (am *AccountManager) NewAccount(passphrase string) (*Account, error) {
|
||||
account, err := am.manager.NewAccount(passphrase)
|
||||
func (ks *KeyStore) NewAccount(passphrase string) (*Account, error) {
|
||||
account, err := ks.keystore.NewAccount(passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -154,13 +171,13 @@ func (am *AccountManager) NewAccount(passphrase string) (*Account, error) {
|
||||
}
|
||||
|
||||
// ExportKey exports as a JSON key, encrypted with newPassphrase.
|
||||
func (am *AccountManager) ExportKey(account *Account, passphrase, newPassphrase string) (key []byte, _ error) {
|
||||
return am.manager.Export(account.account, passphrase, newPassphrase)
|
||||
func (ks *KeyStore) ExportKey(account *Account, passphrase, newPassphrase string) (key []byte, _ error) {
|
||||
return ks.keystore.Export(account.account, passphrase, newPassphrase)
|
||||
}
|
||||
|
||||
// ImportKey stores the given encrypted JSON key into the key directory.
|
||||
func (am *AccountManager) ImportKey(keyJSON []byte, passphrase, newPassphrase string) (account *Account, _ error) {
|
||||
acc, err := am.manager.Import(keyJSON, passphrase, newPassphrase)
|
||||
func (ks *KeyStore) ImportKey(keyJSON []byte, passphrase, newPassphrase string) (account *Account, _ error) {
|
||||
acc, err := ks.keystore.Import(keyJSON, passphrase, newPassphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -168,14 +185,14 @@ func (am *AccountManager) ImportKey(keyJSON []byte, passphrase, newPassphrase st
|
||||
}
|
||||
|
||||
// UpdateAccount changes the passphrase of an existing account.
|
||||
func (am *AccountManager) UpdateAccount(account *Account, passphrase, newPassphrase string) error {
|
||||
return am.manager.Update(account.account, passphrase, newPassphrase)
|
||||
func (ks *KeyStore) UpdateAccount(account *Account, passphrase, newPassphrase string) error {
|
||||
return ks.keystore.Update(account.account, passphrase, newPassphrase)
|
||||
}
|
||||
|
||||
// ImportPreSaleKey decrypts the given Ethereum presale wallet and stores
|
||||
// a key file in the key directory. The key file is encrypted with the same passphrase.
|
||||
func (am *AccountManager) ImportPreSaleKey(keyJSON []byte, passphrase string) (ccount *Account, _ error) {
|
||||
account, err := am.manager.ImportPreSaleKey(keyJSON, passphrase)
|
||||
func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (ccount *Account, _ error) {
|
||||
account, err := ks.keystore.ImportPreSaleKey(keyJSON, passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -43,42 +43,46 @@ public class AndroidTest extends InstrumentationTestCase {
|
||||
public AndroidTest() {}
|
||||
|
||||
public void testAccountManagement() {
|
||||
// Create an encrypted keystore manager with light crypto parameters.
|
||||
AccountManager am = new AccountManager(getInstrumentation().getContext().getFilesDir() + "/keystore", Geth.LightScryptN, Geth.LightScryptP);
|
||||
// Create an encrypted keystore with light crypto parameters.
|
||||
KeyStore ks = new KeyStore(getInstrumentation().getContext().getFilesDir() + "/keystore", Geth.LightScryptN, Geth.LightScryptP);
|
||||
|
||||
try {
|
||||
// Create a new account with the specified encryption passphrase.
|
||||
Account newAcc = am.newAccount("Creation password");
|
||||
Account newAcc = ks.newAccount("Creation password");
|
||||
|
||||
// Export the newly created account with a different passphrase. The returned
|
||||
// data from this method invocation is a JSON encoded, encrypted key-file.
|
||||
byte[] jsonAcc = am.exportKey(newAcc, "Creation password", "Export password");
|
||||
byte[] jsonAcc = ks.exportKey(newAcc, "Creation password", "Export password");
|
||||
|
||||
// Update the passphrase on the account created above inside the local keystore.
|
||||
am.updateAccount(newAcc, "Creation password", "Update password");
|
||||
ks.updateAccount(newAcc, "Creation password", "Update password");
|
||||
|
||||
// Delete the account updated above from the local keystore.
|
||||
am.deleteAccount(newAcc, "Update password");
|
||||
ks.deleteAccount(newAcc, "Update password");
|
||||
|
||||
// Import back the account we've exported (and then deleted) above with yet
|
||||
// again a fresh passphrase.
|
||||
Account impAcc = am.importKey(jsonAcc, "Export password", "Import password");
|
||||
Account impAcc = ks.importKey(jsonAcc, "Export password", "Import password");
|
||||
|
||||
// Create a new account to sign transactions with
|
||||
Account signer = am.newAccount("Signer password");
|
||||
Hash txHash = new Hash("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
|
||||
Account signer = ks.newAccount("Signer password");
|
||||
|
||||
Transaction tx = new Transaction(
|
||||
1, new Address("0x0000000000000000000000000000000000000000"),
|
||||
new BigInt(0), new BigInt(0), new BigInt(1), null); // Random empty transaction
|
||||
BigInt chain = new BigInt(1); // Chain identifier of the main net
|
||||
|
||||
// Sign a transaction with a single authorization
|
||||
byte[] signature = am.signPassphrase(signer, "Signer password", txHash.getBytes());
|
||||
Transaction signed = ks.signTxPassphrase(signer, "Signer password", tx, chain);
|
||||
|
||||
// Sign a transaction with multiple manually cancelled authorizations
|
||||
am.unlock(signer, "Signer password");
|
||||
signature = am.sign(signer.getAddress(), txHash.getBytes());
|
||||
am.lock(signer.getAddress());
|
||||
ks.unlock(signer, "Signer password");
|
||||
signed = ks.signTx(signer, tx, chain);
|
||||
ks.lock(signer.getAddress());
|
||||
|
||||
// Sign a transaction with multiple automatically cancelled authorizations
|
||||
am.timedUnlock(signer, "Signer password", 1000000000);
|
||||
signature = am.sign(signer.getAddress(), txHash.getBytes());
|
||||
ks.timedUnlock(signer, "Signer password", 1000000000);
|
||||
signed = ks.signTx(signer, tx, chain);
|
||||
} catch (Exception e) {
|
||||
fail(e.toString());
|
||||
}
|
||||
|
||||
@@ -132,6 +132,11 @@ type Transaction struct {
|
||||
tx *types.Transaction
|
||||
}
|
||||
|
||||
// NewTransaction creates a new transaction with the given properties.
|
||||
func NewTransaction(nonce int64, to *Address, amount, gasLimit, gasPrice *BigInt, data []byte) *Transaction {
|
||||
return &Transaction{types.NewTransaction(uint64(nonce), to.address, amount.bigint, gasLimit.bigint, gasPrice.bigint, data)}
|
||||
}
|
||||
|
||||
func (tx *Transaction) GetData() []byte { return tx.tx.Data() }
|
||||
func (tx *Transaction) GetGas() int64 { return tx.tx.Gas().Int64() }
|
||||
func (tx *Transaction) GetGasPrice() *BigInt { return &BigInt{tx.tx.GasPrice()} }
|
||||
|
||||
@@ -27,6 +27,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/accounts/usbwallet"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
@@ -400,15 +402,19 @@ func (c *Config) parsePersistentNodes(path string) []*discover.Node {
|
||||
return nodes
|
||||
}
|
||||
|
||||
func makeAccountManager(conf *Config) (am *accounts.Manager, ephemeralKeystore string, err error) {
|
||||
scryptN := accounts.StandardScryptN
|
||||
scryptP := accounts.StandardScryptP
|
||||
func makeAccountManager(conf *Config) (*accounts.Manager, string, error) {
|
||||
scryptN := keystore.StandardScryptN
|
||||
scryptP := keystore.StandardScryptP
|
||||
if conf.UseLightweightKDF {
|
||||
scryptN = accounts.LightScryptN
|
||||
scryptP = accounts.LightScryptP
|
||||
scryptN = keystore.LightScryptN
|
||||
scryptP = keystore.LightScryptP
|
||||
}
|
||||
|
||||
var keydir string
|
||||
var (
|
||||
keydir string
|
||||
ephemeral string
|
||||
err error
|
||||
)
|
||||
switch {
|
||||
case filepath.IsAbs(conf.KeyStoreDir):
|
||||
keydir = conf.KeyStoreDir
|
||||
@@ -423,7 +429,7 @@ func makeAccountManager(conf *Config) (am *accounts.Manager, ephemeralKeystore s
|
||||
default:
|
||||
// There is no datadir.
|
||||
keydir, err = ioutil.TempDir("", "go-ethereum-keystore")
|
||||
ephemeralKeystore = keydir
|
||||
ephemeral = keydir
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
@@ -431,6 +437,14 @@ func makeAccountManager(conf *Config) (am *accounts.Manager, ephemeralKeystore s
|
||||
if err := os.MkdirAll(keydir, 0700); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return accounts.NewManager(keydir, scryptN, scryptP), ephemeralKeystore, nil
|
||||
// Assemble the account manager and supported backends
|
||||
backends := []accounts.Backend{
|
||||
keystore.NewKeyStore(keydir, scryptN, scryptP),
|
||||
}
|
||||
if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
|
||||
glog.V(logger.Warn).Infof("Failed to start Ledger hub, disabling: %v", err)
|
||||
} else {
|
||||
backends = append(backends, ledgerhub)
|
||||
}
|
||||
return accounts.NewManager(backends...), ephemeral, nil
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import "fmt"
|
||||
const (
|
||||
VersionMajor = 1 // Major version component of the current release
|
||||
VersionMinor = 5 // Minor version component of the current release
|
||||
VersionPatch = 8 // Patch version component of the current release
|
||||
VersionPatch = 9 // Patch version component of the current release
|
||||
VersionMeta = "stable" // Version metadata to append to the version string
|
||||
)
|
||||
|
||||
|
||||
@@ -682,7 +682,7 @@ func newClientSubscription(c *Client, channel reflect.Value) *ClientSubscription
|
||||
// resubscription when the client connection is closed unexpectedly.
|
||||
//
|
||||
// The error channel receives a value when the subscription has ended due
|
||||
// to an error. The received error is ErrClientQuit if Close has been called
|
||||
// to an error. The received error is nil if Close has been called
|
||||
// on the underlying client and no other error has occurred.
|
||||
//
|
||||
// The error channel is closed when Unsubscribe is called on the subscription.
|
||||
@@ -707,6 +707,9 @@ func (sub *ClientSubscription) quitWithError(err error, unsubscribeServer bool)
|
||||
sub.requestUnsubscribe()
|
||||
}
|
||||
if err != nil {
|
||||
if err == ErrClientQuit {
|
||||
err = nil // Adhere to subscription semantics.
|
||||
}
|
||||
sub.err <- err
|
||||
}
|
||||
})
|
||||
|
||||
@@ -99,6 +99,7 @@ func (self *Depo) HandleDeliveryRequestMsg(req *deliveryRequestMsgData, p *peer)
|
||||
// if key found locally, return. otherwise
|
||||
// remote is untrusted, so hash is verified and chunk passed on to NetStore
|
||||
func (self *Depo) HandleStoreRequestMsg(req *storeRequestMsgData, p *peer) {
|
||||
var islocal bool
|
||||
req.from = p
|
||||
chunk, err := self.localStore.Get(req.Key)
|
||||
switch {
|
||||
@@ -110,27 +111,32 @@ func (self *Depo) HandleStoreRequestMsg(req *storeRequestMsgData, p *peer) {
|
||||
|
||||
case chunk.SData == nil:
|
||||
// found chunk in memory store, needs the data, validate now
|
||||
hasher := self.hashfunc()
|
||||
hasher.Write(req.SData)
|
||||
if !bytes.Equal(hasher.Sum(nil), req.Key) {
|
||||
// data does not validate, ignore
|
||||
// TODO: peer should be penalised/dropped?
|
||||
glog.V(logger.Warn).Infof("Depo.HandleStoreRequest: chunk invalid. store request ignored: %v", req)
|
||||
return
|
||||
}
|
||||
glog.V(logger.Detail).Infof("Depo.HandleStoreRequest: %v. request entry found", req)
|
||||
|
||||
default:
|
||||
// data is found, store request ignored
|
||||
// this should update access count?
|
||||
glog.V(logger.Detail).Infof("Depo.HandleStoreRequest: %v found locally. ignore.", req)
|
||||
islocal = true
|
||||
//return
|
||||
}
|
||||
|
||||
hasher := self.hashfunc()
|
||||
hasher.Write(req.SData)
|
||||
if !bytes.Equal(hasher.Sum(nil), req.Key) {
|
||||
// data does not validate, ignore
|
||||
// TODO: peer should be penalised/dropped?
|
||||
glog.V(logger.Warn).Infof("Depo.HandleStoreRequest: chunk invalid. store request ignored: %v", req)
|
||||
return
|
||||
}
|
||||
|
||||
if islocal {
|
||||
return
|
||||
}
|
||||
// update chunk with size and data
|
||||
chunk.SData = req.SData // protocol validates that SData is minimum 9 bytes long (int64 size + at least one byte of data)
|
||||
chunk.Size = int64(binary.LittleEndian.Uint64(req.SData[0:8]))
|
||||
glog.V(logger.Detail).Infof("delivery of %p from %v", chunk, p)
|
||||
glog.V(logger.Detail).Infof("delivery of %v from %v", chunk, p)
|
||||
chunk.Source = p
|
||||
self.netStore.Put(chunk)
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@ LOOP:
|
||||
for priority = High; priority >= 0; priority-- {
|
||||
// the first priority channel that is non-empty will be assigned to keys
|
||||
if len(self.keys[priority]) > 0 {
|
||||
glog.V(logger.Detail).Infof("syncer[%v]: reading request with priority %v", self.key.Log(), priority)
|
||||
glog.V(logger.Detail).Infof("syncer[%v]: reading request with priority %v", self.key.Log(), priority)
|
||||
keys = self.keys[priority]
|
||||
break PRIORITIES
|
||||
}
|
||||
@@ -551,10 +551,10 @@ LOOP:
|
||||
}
|
||||
if sreq, err := self.newSyncRequest(req, priority); err == nil {
|
||||
// extract key from req
|
||||
glog.V(logger.Detail).Infof("syncer(priority %v): request %v (synced = %v)", self.key.Log(), priority, req, state.Synced)
|
||||
glog.V(logger.Detail).Infof("syncer[%v]: (priority %v): request %v (synced = %v)", self.key.Log(), priority, req, state.Synced)
|
||||
unsynced = append(unsynced, sreq)
|
||||
} else {
|
||||
glog.V(logger.Warn).Infof("syncer(priority %v): error creating request for %v: %v)", self.key.Log(), priority, req, state.Synced, err)
|
||||
glog.V(logger.Warn).Infof("syncer[%v]: (priority %v): error creating request for %v: %v)", self.key.Log(), priority, req, state.Synced, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -252,12 +252,7 @@ func (s *DbStore) collectGarbage(ratio float32) {
|
||||
// actual gc
|
||||
for i := 0; i < gcnt; i++ {
|
||||
if s.gcArray[i].value <= cutval {
|
||||
batch := new(leveldb.Batch)
|
||||
batch.Delete(s.gcArray[i].idxKey)
|
||||
batch.Delete(getDataKey(s.gcArray[i].idx))
|
||||
s.entryCnt--
|
||||
batch.Put(keyEntryCnt, U64ToBytes(s.entryCnt))
|
||||
s.db.Write(batch)
|
||||
s.delete(s.gcArray[i].idx, s.gcArray[i].idxKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +261,52 @@ func (s *DbStore) collectGarbage(ratio float32) {
|
||||
s.db.Put(keyGCPos, s.gcPos)
|
||||
}
|
||||
|
||||
func (s *DbStore) Cleanup() {
|
||||
//Iterates over the database and checks that there are no faulty chunks
|
||||
it := s.db.NewIterator()
|
||||
startPosition := []byte{kpIndex}
|
||||
it.Seek(startPosition)
|
||||
var key []byte
|
||||
var errorsFound, total int
|
||||
for it.Valid() {
|
||||
key = it.Key()
|
||||
if (key == nil) || (key[0] != kpIndex) {
|
||||
break
|
||||
}
|
||||
total++
|
||||
var index dpaDBIndex
|
||||
decodeIndex(it.Value(), &index)
|
||||
|
||||
data, err := s.db.Get(getDataKey(index.Idx))
|
||||
if err != nil {
|
||||
glog.V(logger.Warn).Infof("Chunk %x found but could not be accessed: %v", key[:], err)
|
||||
s.delete(index.Idx, getIndexKey(key[1:]))
|
||||
errorsFound++
|
||||
} else {
|
||||
hasher := s.hashfunc()
|
||||
hasher.Write(data)
|
||||
hash := hasher.Sum(nil)
|
||||
if !bytes.Equal(hash, key[1:]) {
|
||||
glog.V(logger.Warn).Infof("Found invalid chunk. Hash mismatch. hash=%x, key=%x", hash, key[:])
|
||||
s.delete(index.Idx, getIndexKey(key[1:]))
|
||||
errorsFound++
|
||||
}
|
||||
}
|
||||
it.Next()
|
||||
}
|
||||
it.Release()
|
||||
glog.V(logger.Warn).Infof("Found %v errors out of %v entries", errorsFound, total)
|
||||
}
|
||||
|
||||
func (s *DbStore) delete(idx uint64, idxKey []byte) {
|
||||
batch := new(leveldb.Batch)
|
||||
batch.Delete(idxKey)
|
||||
batch.Delete(getDataKey(idx))
|
||||
s.entryCnt--
|
||||
batch.Put(keyEntryCnt, U64ToBytes(s.entryCnt))
|
||||
s.db.Write(batch)
|
||||
}
|
||||
|
||||
func (s *DbStore) Counter() uint64 {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
@@ -283,6 +324,7 @@ func (s *DbStore) Put(chunk *Chunk) {
|
||||
if chunk.dbStored != nil {
|
||||
close(chunk.dbStored)
|
||||
}
|
||||
glog.V(logger.Detail).Infof("Storing to DB: chunk already exists, only update access")
|
||||
return // already exists, only update access
|
||||
}
|
||||
|
||||
@@ -348,6 +390,8 @@ func (s *DbStore) Get(key Key) (chunk *Chunk, err error) {
|
||||
var data []byte
|
||||
data, err = s.db.Get(getDataKey(index.Idx))
|
||||
if err != nil {
|
||||
glog.V(logger.Detail).Infof("DBStore: Chunk %v found but could not be accessed: %v", key.Log(), err)
|
||||
s.delete(index.Idx, getIndexKey(key))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -355,9 +399,8 @@ func (s *DbStore) Get(key Key) (chunk *Chunk, err error) {
|
||||
hasher.Write(data)
|
||||
hash := hasher.Sum(nil)
|
||||
if !bytes.Equal(hash, key) {
|
||||
s.db.Delete(getDataKey(index.Idx))
|
||||
err = fmt.Errorf("invalid chunk. hash=%x, key=%v", hash, key[:])
|
||||
return
|
||||
s.delete(index.Idx, getIndexKey(key))
|
||||
panic("Invalid Chunk in Database. Please repair with command: 'swarm cleandb'")
|
||||
}
|
||||
|
||||
chunk = &Chunk{
|
||||
@@ -408,7 +451,7 @@ func (s *DbStore) getEntryCnt() uint64 {
|
||||
return s.entryCnt
|
||||
}
|
||||
|
||||
func (s *DbStore) close() {
|
||||
func (s *DbStore) Close() {
|
||||
s.db.Close()
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ func initDbStore(t *testing.T) *DbStore {
|
||||
|
||||
func testDbStore(l int64, branches int64, t *testing.T) {
|
||||
m := initDbStore(t)
|
||||
defer m.close()
|
||||
defer m.Close()
|
||||
testStore(m, l, branches, t)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestDbStore2_100_(t *testing.T) {
|
||||
|
||||
func TestDbStoreNotFound(t *testing.T) {
|
||||
m := initDbStore(t)
|
||||
defer m.close()
|
||||
defer m.Close()
|
||||
_, err := m.Get(ZeroKey)
|
||||
if err != notFound {
|
||||
t.Errorf("Expected notFound, got %v", err)
|
||||
@@ -73,7 +73,7 @@ func TestDbStoreNotFound(t *testing.T) {
|
||||
|
||||
func TestDbStoreSyncIterator(t *testing.T) {
|
||||
m := initDbStore(t)
|
||||
defer m.close()
|
||||
defer m.Close()
|
||||
keys := []Key{
|
||||
Key(common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000000")),
|
||||
Key(common.Hex2Bytes("4000000000000000000000000000000000000000000000000000000000000000")),
|
||||
|
||||
@@ -237,3 +237,8 @@ func (self *dpaChunkStore) Put(entry *Chunk) {
|
||||
self.n++
|
||||
self.netStore.Put(chunk)
|
||||
}
|
||||
|
||||
// Close chunk store
|
||||
func (self *dpaChunkStore) Close() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -72,3 +72,8 @@ func (self *LocalStore) Get(key Key) (chunk *Chunk, err error) {
|
||||
self.memStore.Put(chunk)
|
||||
return
|
||||
}
|
||||
|
||||
// Close local store
|
||||
func (self *LocalStore) Close() {
|
||||
return
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user