Initial commit.
This commit is contained in:
commit
a56e4e89d2
9 changed files with 1773 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
btcwallet
|
95
README.md
Normal file
95
README.md
Normal file
|
@ -0,0 +1,95 @@
|
|||
btcwallet
|
||||
=========
|
||||
|
||||
btcwallet is a daemon handling bitcoin wallet functions. It relies on
|
||||
a running btcd instance for asynchronous blockchain queries and
|
||||
notifications over websockets.
|
||||
|
||||
In addition to the HTTP server run by btcd to provide RPC and
|
||||
websocket connections, btcwallet requires an HTTP server of its own to
|
||||
provide websocket connections to wallet frontends. Websockets allow for
|
||||
asynchronous queries, replies, and notifications.
|
||||
|
||||
This project is currently under active development is not production
|
||||
ready yet.
|
||||
|
||||
## Usage
|
||||
|
||||
Frontends wishing to use btcwallet must connect to the websocket
|
||||
`/wallet`. Messages sent to btcwallet over this websocket are
|
||||
expected to follow the standard [Bitcoin JSON
|
||||
API](https://en.bitcoin.it/wiki/Original_Bitcoin_client/API_Calls_list)
|
||||
and replies follow the same API. The btcd package `btcjson` provides
|
||||
types and functions for creating messages that this API. However, due
|
||||
to taking a synchronous protocol like RPC and using it asynchronously,
|
||||
it is recommend for frontends to use the JSON `id` field as a sequence
|
||||
number so replies can be mapped back to the messages they originated
|
||||
from.
|
||||
|
||||
## Installation
|
||||
|
||||
btcwallet can be installed with the go get command:
|
||||
|
||||
```bash
|
||||
go get github.com/conformal/btcwallet
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
To run btcwallet, you must have btcd installed and running. By
|
||||
default btcd will run its HTTP server for RPC and websocket
|
||||
connections on port 8332. However, bitcoind frontends expecting
|
||||
wallet functionality may require to poll on port 8332, requiring the
|
||||
btcd component in a btcwallet+btcd replacement stack to run on an
|
||||
alternate port. For this reason, btcwallet by default connects to
|
||||
btcd on port 8334 and runs its own HTTP server on 8332. When using
|
||||
both btcd and btcwallet, it is recommend to run btcd on the
|
||||
non-standard port 8334 using the `-r` command line flag.
|
||||
|
||||
Assumming btcd is running on port 8334, btcwallet can be
|
||||
started by running:
|
||||
|
||||
```bash
|
||||
btcwallet -f /path/to/wallet
|
||||
```
|
||||
|
||||
## GPG Verification Key
|
||||
|
||||
All official release tags are signed by Conformal so users can ensure the code
|
||||
has not been tampered with and is coming from Conformal. To verify the
|
||||
signature perform the following:
|
||||
|
||||
- Download the public key from the Conformal website at
|
||||
https://opensource.conformal.com/GIT-GPG-KEY-conformal.txt
|
||||
|
||||
- Import the public key into your GPG keyring:
|
||||
```bash
|
||||
gpg --import GIT-GPG-KEY-conformal.txt
|
||||
```
|
||||
|
||||
- Verify the release tag with the following command where `TAG_NAME` is a
|
||||
placeholder for the specific tag:
|
||||
```bash
|
||||
git tag -v TAG_NAME
|
||||
```
|
||||
|
||||
## What works
|
||||
- New addresses can be queried if they are in the wallet file address pool
|
||||
- Unknown commands are sent to btcd
|
||||
- Unhandled btcd notifications (i.e. new blockchain height) are sent to each
|
||||
connected frontend
|
||||
- btcd replies are routed back to the correct frontend who initiated the request
|
||||
|
||||
## TODO
|
||||
- Create a new wallet if one is not available
|
||||
- Update UTXO database based on btcd notifications
|
||||
- Require authentication before wallet functionality can be accessed
|
||||
- Support TLS
|
||||
- Documentation
|
||||
- Code cleanup
|
||||
- Optimize
|
||||
- Much much more. Stay tuned.
|
||||
|
||||
## License
|
||||
|
||||
btcwallet is licensed under the liberal ISC License.
|
66
cmd.go
Normal file
66
cmd.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/conformal/btcwallet/wallet"
|
||||
"github.com/conformal/seelog"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
log seelog.LoggerInterface = seelog.Default
|
||||
cfg *config
|
||||
wallets = make(map[string]*wallet.Wallet)
|
||||
)
|
||||
|
||||
func main() {
|
||||
tcfg, _, err := loadConfig()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg = tcfg
|
||||
|
||||
// Open wallet
|
||||
file, err := os.Open(cfg.WalletFile)
|
||||
if err != nil {
|
||||
log.Error("Error opening wallet:", err)
|
||||
}
|
||||
w := new(wallet.Wallet)
|
||||
if _, err = w.ReadFrom(file); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
// Associate this wallet with default account.
|
||||
wallets[""] = w
|
||||
|
||||
// Start HTTP server to listen and send messages to frontend and btcd
|
||||
// backend. Try reconnection if connection failed.
|
||||
for {
|
||||
if err := ListenAndServe(); err == ConnRefused {
|
||||
// wait and try again.
|
||||
log.Info("Unable to connect to btcd. Retrying in 5 seconds.")
|
||||
time.Sleep(5 * time.Second)
|
||||
} else if err != nil {
|
||||
log.Info(err.Error())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
176
cmdmgr.go
Normal file
176
cmdmgr.go
Normal file
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/conformal/btcjson"
|
||||
"time"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
// seq holds the btcwallet sequence number for frontend messages
|
||||
// which must be sent to and received from btcd. A Mutex protects
|
||||
// against concurrent access.
|
||||
seq = struct {
|
||||
sync.Mutex
|
||||
n uint64
|
||||
}{}
|
||||
|
||||
// replyRouter maps uint64 ids to reply channels, so btcd replies can
|
||||
// be routed to the correct frontend.
|
||||
replyRouter = struct {
|
||||
sync.Mutex
|
||||
m map[uint64]chan []byte
|
||||
}{
|
||||
m: make(map[uint64]chan []byte),
|
||||
}
|
||||
)
|
||||
|
||||
// ProcessFrontendMsg checks the message sent from a frontend. If the
|
||||
// message method is one that must be handled by btcwallet, the request
|
||||
// is processed here. Otherwise, the message is sent to btcd.
|
||||
func ProcessFrontendMsg(reply chan []byte, msg []byte) {
|
||||
cmd, err := btcjson.JSONGetMethod(msg)
|
||||
if err != nil {
|
||||
log.Error("Unable to parse JSON method from message.")
|
||||
return
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "getaddressesbyaccount":
|
||||
GetAddressesByAccount(reply, msg)
|
||||
case "getnewaddress":
|
||||
GetNewAddress(reply, msg)
|
||||
case "walletlock":
|
||||
WalletLock(reply, msg)
|
||||
case "walletpassphrase":
|
||||
WalletPassphrase(reply, msg)
|
||||
default:
|
||||
// btcwallet does not understand method. Pass to btcd.
|
||||
log.Info("Unknown btcwallet method", cmd)
|
||||
|
||||
seq.Lock()
|
||||
n := seq.n
|
||||
seq.n++
|
||||
seq.Unlock()
|
||||
|
||||
var m map[string]interface{}
|
||||
json.Unmarshal(msg, &m)
|
||||
m["id"] = fmt.Sprintf("btcwallet(%v)-%v", n, m["id"])
|
||||
newMsg, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
log.Info("Error marshalling json: " + err.Error())
|
||||
}
|
||||
replyRouter.Lock()
|
||||
replyRouter.m[n] = reply
|
||||
replyRouter.Unlock()
|
||||
btcdMsgs <- newMsg
|
||||
}
|
||||
}
|
||||
|
||||
// GetAddressesByAccount Gets all addresses for an account.
|
||||
func GetAddressesByAccount(reply chan []byte, msg []byte) {
|
||||
var v map[string]interface{}
|
||||
json.Unmarshal(msg, &v)
|
||||
params := v["params"].([]interface{})
|
||||
id := v["id"]
|
||||
r := btcjson.Reply{
|
||||
Id: &id,
|
||||
}
|
||||
if w := wallets[params[0].(string)]; w != nil {
|
||||
r.Result = w.GetActiveAddresses()
|
||||
} else {
|
||||
r.Result = []interface{}{}
|
||||
}
|
||||
mr, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
log.Info("Error marshalling reply: %v", err)
|
||||
return
|
||||
}
|
||||
reply <- mr
|
||||
}
|
||||
|
||||
// GetNewAddress gets or generates a new address for an account.
|
||||
//
|
||||
// TODO(jrick): support non-default account wallets.
|
||||
func GetNewAddress(reply chan []byte, msg []byte) {
|
||||
var v map[string]interface{}
|
||||
json.Unmarshal(msg, &v)
|
||||
params := v["params"].([]interface{})
|
||||
if len(params) == 0 || params[0].(string) == "" {
|
||||
if w := wallets[""]; w != nil {
|
||||
addr := w.NextUnusedAddress()
|
||||
id := v["id"]
|
||||
r := btcjson.Reply{
|
||||
Result: addr,
|
||||
Id: &id,
|
||||
}
|
||||
mr, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
log.Info("Error marshalling reply: %v", err)
|
||||
return
|
||||
}
|
||||
reply <- mr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WalletLock locks the wallet.
|
||||
//
|
||||
// TODO(jrick): figure out how multiple wallets/accounts will work
|
||||
// with this.
|
||||
func WalletLock(reply chan []byte, msg []byte) {
|
||||
// TODO(jrick)
|
||||
}
|
||||
|
||||
|
||||
// WalletPassphrase stores the decryption key for the default account,
|
||||
// unlocking the wallet.
|
||||
//
|
||||
// TODO(jrick): figure out how multiple wallets/accounts will work
|
||||
// with this.
|
||||
func WalletPassphrase(reply chan []byte, msg []byte) {
|
||||
var v map[string]interface{}
|
||||
json.Unmarshal(msg, &v)
|
||||
params := v["params"].([]interface{})
|
||||
if len(params) != 2 {
|
||||
log.Error("walletpasshprase: incorrect parameters")
|
||||
return
|
||||
}
|
||||
passphrase, ok := params[0].(string)
|
||||
if !ok {
|
||||
log.Error("walletpasshprase: incorrect parameters")
|
||||
return
|
||||
}
|
||||
timeout, ok := params[1].(float64)
|
||||
if !ok {
|
||||
log.Error("walletpasshprase: incorrect parameters")
|
||||
return
|
||||
}
|
||||
|
||||
if w := wallets[""]; w != nil {
|
||||
w.Unlock([]byte(passphrase))
|
||||
go func() {
|
||||
time.Sleep(time.Second * time.Duration(int64(timeout)))
|
||||
fmt.Println("finally locking")
|
||||
w.Lock()
|
||||
}()
|
||||
}
|
||||
}
|
133
config.go
Normal file
133
config.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/conformal/go-flags"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultConfigFilename = "btcwallet.conf"
|
||||
defaultBtcdPort = 8334
|
||||
defaultLogLevel = "info"
|
||||
defaultServerPort = 8332
|
||||
)
|
||||
|
||||
var (
|
||||
defaultConfigFile = filepath.Join(btcwalletHomeDir(), defaultConfigFilename)
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"`
|
||||
BtcdPort int `short:"b" long:"btcdport" description:"Port to connect to btcd on"`
|
||||
DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"`
|
||||
ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"`
|
||||
SvrPort int `short:"p" long:"serverport" description:"Port to serve frontend websocket connections on"`
|
||||
WalletFile string `short:"f" long:"walletfile" description:"Path to wallet file"`
|
||||
}
|
||||
|
||||
// btcwalletHomeDir returns an OS appropriate home directory for btcwallet.
|
||||
func btcwalletHomeDir() string {
|
||||
// Search for Windows APPDATA first. This won't exist on POSIX OSes.
|
||||
appData := os.Getenv("APPDATA")
|
||||
if appData != "" {
|
||||
return filepath.Join(appData, "btcwallet")
|
||||
}
|
||||
|
||||
// Fall back to standard HOME directory that works for most POSIX OSes.
|
||||
home := os.Getenv("HOME")
|
||||
if home != "" {
|
||||
return filepath.Join(home, ".btcwallet")
|
||||
}
|
||||
|
||||
// In the worst case, use the current directory.
|
||||
return "."
|
||||
}
|
||||
|
||||
// filesExists reports whether the named file or directory exists.
|
||||
func fileExists(name string) bool {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// loadConfig initializes and parses the config using a config file and command
|
||||
// line options.
|
||||
//
|
||||
// The configuration proceeds as follows:
|
||||
// 1) Start with a default config with sane settings
|
||||
// 2) Pre-parse the command line to check for an alternative config file
|
||||
// 3) Load configuration file overwriting defaults with any specified options
|
||||
// 4) Parse CLI options and overwrite/add any specified options
|
||||
//
|
||||
// The above results in btcwallet functioning properly without any config
|
||||
// settings while still allowing the user to override settings with config files
|
||||
// and command line options. Command line options always take precedence.
|
||||
func loadConfig() (*config, []string, error) {
|
||||
// Default config.
|
||||
cfg := config{
|
||||
DebugLevel: defaultLogLevel,
|
||||
ConfigFile: defaultConfigFile,
|
||||
BtcdPort: defaultBtcdPort,
|
||||
SvrPort: defaultServerPort,
|
||||
}
|
||||
|
||||
// A config file in the current directory takes precedence.
|
||||
if fileExists(defaultConfigFilename) {
|
||||
cfg.ConfigFile = defaultConfigFile
|
||||
}
|
||||
|
||||
// Pre-parse the command line options to see if an alternative config
|
||||
// file or the version flag was specified.
|
||||
preCfg := cfg
|
||||
preParser := flags.NewParser(&preCfg, flags.Default)
|
||||
_, err := preParser.Parse()
|
||||
if err != nil {
|
||||
if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
|
||||
preParser.WriteHelp(os.Stderr)
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Show the version and exit if the version flag was specified.
|
||||
if preCfg.ShowVersion {
|
||||
appName := filepath.Base(os.Args[0])
|
||||
appName = strings.TrimSuffix(appName, filepath.Ext(appName))
|
||||
fmt.Println(appName, "version", version())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Load additional config from file.
|
||||
parser := flags.NewParser(&cfg, flags.Default)
|
||||
err = parser.ParseIniFile(preCfg.ConfigFile)
|
||||
if err != nil {
|
||||
if _, ok := err.(*os.PathError); !ok {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
parser.WriteHelp(os.Stderr)
|
||||
return nil, nil, err
|
||||
}
|
||||
log.Warnf("%v", err)
|
||||
}
|
||||
|
||||
// Parse command line options again to ensure they take precedence.
|
||||
remainingArgs, err := parser.Parse()
|
||||
if err != nil {
|
||||
if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
|
||||
parser.WriteHelp(os.Stderr)
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// wallet file must be valid
|
||||
if !fileExists(cfg.WalletFile) {
|
||||
return &cfg, nil, errors.New("Wallet file does not exist.")
|
||||
}
|
||||
|
||||
return &cfg, remainingArgs, nil
|
||||
}
|
361
sockets.go
Normal file
361
sockets.go
Normal file
|
@ -0,0 +1,361 @@
|
|||
/*
|
||||
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"code.google.com/p/go.net/websocket"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/conformal/btcjson"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
ConnRefused = errors.New("Connection refused")
|
||||
|
||||
// Channel to close to notify that connection to btcd has been lost.
|
||||
btcdDisconnected = make(chan int)
|
||||
|
||||
// Channel to send messages btcwallet does not understand to btcd.
|
||||
btcdMsgs = make(chan []byte, 100)
|
||||
|
||||
// Adds a frontend listener channel
|
||||
addFrontendListener = make(chan (chan []byte))
|
||||
|
||||
// Removes a frontend listener channel
|
||||
deleteFrontendListener = make(chan (chan []byte))
|
||||
|
||||
// Messages sent to this channel are sent to each connected frontend.
|
||||
frontendNotificationMaster = make(chan []byte, 100)
|
||||
|
||||
replyHandlers = struct {
|
||||
sync.Mutex
|
||||
m map[uint64]func(interface{}) bool
|
||||
}{
|
||||
m: make(map[uint64]func(interface{}) bool),
|
||||
}
|
||||
)
|
||||
|
||||
// frontendListenerDuplicator listens for new wallet listener channels
|
||||
// and duplicates messages sent to frontendNotificationMaster to all
|
||||
// connected listeners.
|
||||
func frontendListenerDuplicator() {
|
||||
// frontendListeners is a map holding each currently connected frontend
|
||||
// listener as the key. The value is ignored, as this is only used as
|
||||
// a set.
|
||||
frontendListeners := make(map[chan []byte]bool)
|
||||
|
||||
// Don't want to add or delete a wallet listener while iterating
|
||||
// through each to propigate to every attached wallet. Use a mutex to
|
||||
// prevent this.
|
||||
mtx := new(sync.Mutex)
|
||||
|
||||
// Check for listener channels to add or remove from set.
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case c := <-addFrontendListener:
|
||||
mtx.Lock()
|
||||
frontendListeners[c] = true
|
||||
mtx.Unlock()
|
||||
case c := <-deleteFrontendListener:
|
||||
mtx.Lock()
|
||||
delete(frontendListeners, c)
|
||||
mtx.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Duplicate all messages sent across frontendNotificationMaster to each
|
||||
// listening wallet.
|
||||
for {
|
||||
ntfn := <-frontendNotificationMaster
|
||||
mtx.Lock()
|
||||
for c, _ := range frontendListeners {
|
||||
c <- ntfn
|
||||
}
|
||||
mtx.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// frontendReqsNotifications is the handler function for websocket
|
||||
// connections from a btcwallet instance. It reads messages from wallet and
|
||||
// sends back replies, as well as notififying wallets of chain updates.
|
||||
// There can possibly be many of these running, one for each currently
|
||||
// connected frontend.
|
||||
func frontendReqsNotifications(ws *websocket.Conn) {
|
||||
// Add frontend notification channel to set so this handler receives
|
||||
// updates.
|
||||
frontendNotification := make(chan []byte)
|
||||
addFrontendListener <- frontendNotification
|
||||
defer func() {
|
||||
deleteFrontendListener <- frontendNotification
|
||||
}()
|
||||
|
||||
// jsonMsgs receives JSON messages from the currently connected frontend.
|
||||
jsonMsgs := make(chan []byte)
|
||||
|
||||
// Receive messages from websocket and send across jsonMsgs until
|
||||
// connection is lost
|
||||
go func() {
|
||||
for {
|
||||
var m []byte
|
||||
if err := websocket.Message.Receive(ws, &m); err != nil {
|
||||
close(jsonMsgs)
|
||||
return
|
||||
}
|
||||
jsonMsgs <- m
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-btcdDisconnected:
|
||||
var idStr interface{} = "btcwallet:btcddisconnected"
|
||||
r := btcjson.Reply{
|
||||
Id: &idStr,
|
||||
}
|
||||
m, _ := json.Marshal(r)
|
||||
websocket.Message.Send(ws, m)
|
||||
return
|
||||
case m, ok := <-jsonMsgs:
|
||||
if !ok {
|
||||
// frontend disconnected.
|
||||
return
|
||||
}
|
||||
// Handle JSON message here.
|
||||
go ProcessFrontendMsg(frontendNotification, m)
|
||||
case ntfn, _ := <-frontendNotification:
|
||||
if err := websocket.Message.Send(ws, ntfn); err != nil {
|
||||
// Frontend disconnected.
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BtcdHandler listens for replies and notifications from btcd over a
|
||||
// websocket and sends messages that btcwallet does not understand to
|
||||
// btcd. Unlike FrontendHandler, exactly one BtcdHandler goroutine runs.
|
||||
func BtcdHandler(ws *websocket.Conn) {
|
||||
disconnected := make(chan int)
|
||||
|
||||
defer func() {
|
||||
close(disconnected)
|
||||
close(btcdDisconnected)
|
||||
}()
|
||||
|
||||
// Listen for replies/notifications from btcd, and decide how to handle them.
|
||||
replies := make(chan []byte)
|
||||
go func() {
|
||||
defer close(replies)
|
||||
for {
|
||||
select {
|
||||
case <-disconnected:
|
||||
return
|
||||
default:
|
||||
var m []byte
|
||||
if err := websocket.Message.Receive(ws, &m); err != nil {
|
||||
return
|
||||
}
|
||||
replies <- m
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(jrick): hook this up with addresses in wallet.
|
||||
// reqTxsForAddress("addr")
|
||||
|
||||
for {
|
||||
select {
|
||||
case rply, ok := <-replies:
|
||||
if !ok {
|
||||
// btcd disconnected
|
||||
return
|
||||
}
|
||||
// Handle message here.
|
||||
go ProcessBtcdNotificationReply(rply)
|
||||
case r := <-btcdMsgs:
|
||||
if err := websocket.Message.Send(ws, r); err != nil {
|
||||
// btcd disconnected.
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessBtcdNotificationReply unmarshalls the JSON notification or
|
||||
// reply received from btcd and decides how to handle it. Replies are
|
||||
// routed back to the frontend who sent the message, and wallet
|
||||
// notifications are processed by btcwallet, and frontend notifications
|
||||
// are sent to every connected frontend.
|
||||
func ProcessBtcdNotificationReply(b []byte) {
|
||||
// Check if the json id field was set by btcwallet.
|
||||
var routeId uint64
|
||||
var origId string
|
||||
|
||||
var m map[string]interface{}
|
||||
json.Unmarshal(b, &m)
|
||||
idStr, ok := m["id"].(string)
|
||||
if !ok {
|
||||
// btcd should only ever be sending JSON messages with a string in
|
||||
// the id field. Log the error and drop the message.
|
||||
log.Error("Unable to process btcd notification or reply.")
|
||||
return
|
||||
}
|
||||
|
||||
n, _ := fmt.Sscanf(idStr, "btcwallet(%d)-%s", &routeId, &origId)
|
||||
if n == 1 {
|
||||
// Request originated from btcwallet. Run and remove correct
|
||||
// handler.
|
||||
replyHandlers.Lock()
|
||||
f := replyHandlers.m[routeId]
|
||||
replyHandlers.Unlock()
|
||||
if f != nil {
|
||||
go func() {
|
||||
if f(m["result"]) {
|
||||
replyHandlers.Lock()
|
||||
delete(replyHandlers.m, routeId)
|
||||
replyHandlers.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
} else if n == 2 {
|
||||
// Attempt to route btcd reply to correct frontend.
|
||||
replyRouter.Lock()
|
||||
c := replyRouter.m[routeId]
|
||||
if c != nil {
|
||||
delete(replyRouter.m, routeId)
|
||||
} else {
|
||||
// Can't route to a frontend, drop reply.
|
||||
log.Info("Unable to route btcd reply to frontend. Dropping.")
|
||||
return
|
||||
}
|
||||
replyRouter.Unlock()
|
||||
|
||||
// Convert string back to number if possible.
|
||||
var origIdNum float64
|
||||
n, _ := fmt.Sscanf(origId, "%f", &origIdNum)
|
||||
if n == 1 {
|
||||
m["id"] = origIdNum
|
||||
} else {
|
||||
m["id"] = origId
|
||||
}
|
||||
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
log.Error("Error marshalling btcd reply. Dropping.")
|
||||
return
|
||||
}
|
||||
c <- b
|
||||
} else {
|
||||
// btcd notification must either be handled by btcwallet or sent
|
||||
// to all frontends if btcwallet can not handle it.
|
||||
switch idStr {
|
||||
default:
|
||||
frontendNotificationMaster <- b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ListenAndServe connects to a running btcd instance over a websocket
|
||||
// for sending and receiving chain-related messages, failing if the
|
||||
// connection can not be established. An additional HTTP server is then
|
||||
// started to provide websocket connections for any number of btcwallet
|
||||
// frontends.
|
||||
func ListenAndServe() error {
|
||||
// Attempt to connect to running btcd instance. Bail if it fails.
|
||||
btcdws, err := websocket.Dial(
|
||||
fmt.Sprintf("ws://localhost:%d/wallet", cfg.BtcdPort),
|
||||
"",
|
||||
"http://localhost/")
|
||||
if err != nil {
|
||||
return ConnRefused
|
||||
}
|
||||
go BtcdHandler(btcdws)
|
||||
|
||||
log.Info("Established connection to btcd.")
|
||||
|
||||
// We'll need to duplicate replies to frontends to each frontend.
|
||||
// Replies are sent to frontendReplyMaster, and duplicated to each valid
|
||||
// channel in frontendReplySet. This runs a goroutine to duplicate
|
||||
// requests for each channel in the set.
|
||||
go frontendListenerDuplicator()
|
||||
|
||||
// XXX(jrick): We need some sort of authentication before websocket
|
||||
// connections are allowed, and perhaps TLS on the server as well.
|
||||
http.Handle("/frontend", websocket.Handler(frontendReqsNotifications))
|
||||
if err := http.ListenAndServe(fmt.Sprintf(":%d", cfg.SvrPort), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func reqTxsForAddress(addr string) {
|
||||
for i := 0; i < 10; i++ {
|
||||
seq.Lock()
|
||||
n := seq.n
|
||||
seq.n++
|
||||
seq.Unlock()
|
||||
|
||||
id := fmt.Sprintf("btcwallet(%v)", n)
|
||||
msg, err := btcjson.CreateMessageWithId("getblockhash", id, i)
|
||||
if err != nil {
|
||||
fmt.Println(msg)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
replyHandlers.Lock()
|
||||
replyHandlers.m[n] = func(result interface{}) bool {
|
||||
fmt.Println(result)
|
||||
return true
|
||||
}
|
||||
replyHandlers.Unlock()
|
||||
|
||||
btcdMsgs <- msg
|
||||
}
|
||||
|
||||
seq.Lock()
|
||||
n := seq.n
|
||||
seq.n++
|
||||
seq.Unlock()
|
||||
|
||||
m := &btcjson.Message{
|
||||
Jsonrpc: "",
|
||||
Id: fmt.Sprintf("btcwallet(%v)", n),
|
||||
Method: "rescanforutxo",
|
||||
Params: []interface{}{
|
||||
"17XhEvq9Nahdj7Xe1nv6oRe1tEmaHUuynH",
|
||||
},
|
||||
}
|
||||
msg, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
replyHandlers.Lock()
|
||||
replyHandlers.m[n] = func(result interface{}) bool {
|
||||
fmt.Println("result:", result)
|
||||
return result == nil
|
||||
}
|
||||
replyHandlers.Unlock()
|
||||
|
||||
btcdMsgs <- msg
|
||||
}
|
68
version.go
Normal file
68
version.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// semanticAlphabet
|
||||
const semanticAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
|
||||
|
||||
// These constants define the application version and follow the semantic
|
||||
// versioning 2.0.0 spec (http://semver.org/).
|
||||
const (
|
||||
appMajor uint = 0
|
||||
appMinor uint = 1
|
||||
appPatch uint = 0
|
||||
|
||||
// appPreRelease MUST only contain characters from semanticAlphabet
|
||||
// per the semantic versioning spec.
|
||||
appPreRelease = "alpha"
|
||||
)
|
||||
|
||||
// appBuild is defined as a variable so it can be overridden during the build
|
||||
// process with '-ldflags "-X main.appBuild foo' if needed. It MUST only
|
||||
// contain characters from semanticAlphabet per the semantic versioning spec.
|
||||
var appBuild string
|
||||
|
||||
// version returns the application version as a properly formed string per the
|
||||
// semantic versioning 2.0.0 spec (http://semver.org/).
|
||||
func version() string {
|
||||
// Start with the major, minor, and path versions.
|
||||
version := fmt.Sprintf("%d.%d.%d", appMajor, appMinor, appPatch)
|
||||
|
||||
// Append pre-release version if there is one. The hyphen called for
|
||||
// by the semantic versioning spec is automatically appended and should
|
||||
// not be contained in the pre-release string. The pre-release version
|
||||
// is not appended if it contains invalid characters.
|
||||
preRelease := normalizeVerString(appPreRelease)
|
||||
if preRelease != "" {
|
||||
version = fmt.Sprintf("%s-%s", version, preRelease)
|
||||
}
|
||||
|
||||
// Append build metadata if there is any. The plus called for
|
||||
// by the semantic versioning spec is automatically appended and should
|
||||
// not be contained in the build metadata string. The build metadata
|
||||
// string is not appended if it contains invalid characters.
|
||||
build := normalizeVerString(appBuild)
|
||||
if build != "" {
|
||||
version = fmt.Sprintf("%s+%s", version, build)
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
// normalizeVerString returns the passed string stripped of all characters which
|
||||
// are not valid according to the semantic versioning guidelines for pre-release
|
||||
// version and build metadata strings. In particular they MUST only contain
|
||||
// characters in semanticAlphabet.
|
||||
func normalizeVerString(str string) string {
|
||||
var result bytes.Buffer
|
||||
for _, r := range str {
|
||||
if strings.ContainsRune(semanticAlphabet, r) {
|
||||
result.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
813
wallet/wallet.go
Normal file
813
wallet/wallet.go
Normal file
|
@ -0,0 +1,813 @@
|
|||
/*
|
||||
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package wallet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.google.com/p/go.crypto/ripemd160"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/conformal/btcec"
|
||||
"github.com/conformal/btcutil"
|
||||
"github.com/conformal/btcwire"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
// Length in bytes of KDF output.
|
||||
kdfOutputBytes = 32
|
||||
|
||||
// Maximum length in bytes of a comment that can have a size represented
|
||||
// as a uint16.
|
||||
maxCommentLen = (1 << 16) - 1
|
||||
)
|
||||
|
||||
// Possible errors when dealing with wallets.
|
||||
var (
|
||||
ChecksumErr = errors.New("Checksum mismatch")
|
||||
MalformedEntryErr = errors.New("Malformed entry")
|
||||
WalletDoesNotExist = errors.New("Non-existant wallet")
|
||||
)
|
||||
|
||||
type entryHeader byte
|
||||
|
||||
const (
|
||||
addrCommentHeader entryHeader = 1 << iota
|
||||
txCommentHeader
|
||||
deletedHeader
|
||||
addrHeader entryHeader = 0
|
||||
)
|
||||
|
||||
// We want to use binaryRead and binaryWrite instead of binary.Read
|
||||
// and binary.Write because those from the binary package do not return
|
||||
// the number of bytes actually written or read. We need to return
|
||||
// this value to correctly support the io.ReaderFrom and io.WriterTo
|
||||
// interfaces.
|
||||
func binaryRead(r io.Reader, order binary.ByteOrder, data interface{}) (n int64, err error) {
|
||||
var read int
|
||||
buf := make([]byte, binary.Size(data))
|
||||
if read, err = r.Read(buf); err != nil {
|
||||
return int64(read), err
|
||||
}
|
||||
return int64(read), binary.Read(bytes.NewBuffer(buf), order, data)
|
||||
}
|
||||
|
||||
// See comment for binaryRead().
|
||||
func binaryWrite(w io.Writer, order binary.ByteOrder, data interface{}) (n int64, err error) {
|
||||
var buf bytes.Buffer
|
||||
if err = binary.Write(&buf, order, data); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
written, err := w.Write(buf.Bytes())
|
||||
return int64(written), err
|
||||
}
|
||||
|
||||
func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte {
|
||||
saltedpass := append(passphrase, salt...)
|
||||
lutbl := make([]byte, memReqts)
|
||||
|
||||
// Seed for lookup table
|
||||
seed := sha512.Sum512(saltedpass)
|
||||
copy(lutbl[:sha512.Size], seed[:])
|
||||
|
||||
for nByte := 0; nByte < (int(memReqts) - sha512.Size); nByte += sha512.Size {
|
||||
hash := sha512.Sum512(lutbl[nByte : nByte+sha512.Size])
|
||||
copy(lutbl[nByte+sha512.Size:nByte+2*sha512.Size], hash[:])
|
||||
}
|
||||
|
||||
x := lutbl[cap(lutbl)-sha512.Size:]
|
||||
|
||||
seqCt := uint32(memReqts / sha512.Size)
|
||||
nLookups := seqCt / 2
|
||||
for i := uint32(0); i < nLookups; i++ {
|
||||
// Armory ignores endianness here. We assume LE.
|
||||
newIdx := binary.LittleEndian.Uint32(x[cap(x)-4:]) % seqCt
|
||||
|
||||
// Index of hash result at newIdx
|
||||
vIdx := newIdx * sha512.Size
|
||||
v := lutbl[vIdx : vIdx+sha512.Size]
|
||||
|
||||
// XOR hash x with hash v
|
||||
for j := 0; j < sha512.Size; j++ {
|
||||
x[j] ^= v[j]
|
||||
}
|
||||
|
||||
// Save new hash to x
|
||||
hash := sha512.Sum512(x)
|
||||
copy(x, hash[:])
|
||||
}
|
||||
|
||||
return x[:kdfOutputBytes]
|
||||
}
|
||||
|
||||
// Key implements the key derivation function used by Armory
|
||||
// based on the ROMix algorithm described in Colin Percival's paper
|
||||
// "Stronger Key Derivation via Sequential Memory-Hard Functions"
|
||||
// (http://www.tarsnap.com/scrypt/scrypt.pdf).
|
||||
func Key(passphrase, salt []byte, memReqts uint64, nIters uint32) []byte {
|
||||
masterKey := passphrase
|
||||
for i := uint32(0); i < nIters; i++ {
|
||||
masterKey = keyOneIter(masterKey, salt, memReqts)
|
||||
}
|
||||
return masterKey
|
||||
}
|
||||
|
||||
type varEntries []io.WriterTo
|
||||
|
||||
func (v *varEntries) WriteTo(w io.Writer) (n int64, err error) {
|
||||
ss := ([]io.WriterTo)(*v)
|
||||
|
||||
var written int64
|
||||
for _, s := range ss {
|
||||
var err error
|
||||
if written, err = s.WriteTo(w); err != nil {
|
||||
return n + written, err
|
||||
}
|
||||
n += written
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (v *varEntries) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
var read int64
|
||||
|
||||
// Remove any previous entries.
|
||||
*v = nil
|
||||
wts := ([]io.WriterTo)(*v)
|
||||
|
||||
// Keep reading entries until an EOF is reached.
|
||||
for {
|
||||
var header entryHeader
|
||||
if read, err = binaryRead(r, binary.LittleEndian, &header); err != nil {
|
||||
// EOF here is not an error.
|
||||
if err == io.EOF {
|
||||
return n + read, nil
|
||||
}
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
|
||||
var wt io.WriterTo = nil
|
||||
switch header {
|
||||
case addrHeader:
|
||||
var entry addrEntry
|
||||
if read, err = entry.ReadFrom(r); err != nil {
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
wt = &entry
|
||||
case addrCommentHeader:
|
||||
var entry addrCommentEntry
|
||||
if read, err = entry.ReadFrom(r); err != nil {
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
wt = &entry
|
||||
case txCommentHeader:
|
||||
var entry txCommentEntry
|
||||
if read, err = entry.ReadFrom(r); err != nil {
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
wt = &entry
|
||||
case deletedHeader:
|
||||
var entry deletedEntry
|
||||
if read, err = entry.ReadFrom(r); err != nil {
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
default:
|
||||
return n, fmt.Errorf("Unknown entry header: %d", uint8(header))
|
||||
}
|
||||
if wt != nil {
|
||||
wts = append(wts, wt)
|
||||
*v = wts
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Wallet represents an btcd/Armory wallet in memory. It
|
||||
// implements the io.ReaderFrom and io.WriterTo interfaces to read
|
||||
// from and write to any type of byte streams, including files.
|
||||
// TODO(jrick) remove as many more magic numbers as possible.
|
||||
type Wallet struct {
|
||||
fileID [8]byte
|
||||
version uint32
|
||||
netMagicBytes [4]byte
|
||||
walletFlags [8]byte
|
||||
uniqID [6]byte
|
||||
createDate [8]byte
|
||||
name [32]byte
|
||||
description [256]byte
|
||||
highestUsed int64
|
||||
kdfParams kdfParameters
|
||||
encryptionParams [256]byte
|
||||
keyGenerator btcAddress
|
||||
appendedEntries varEntries
|
||||
|
||||
// These are not serialized
|
||||
addrMap map[[ripemd160.Size]byte]*btcAddress
|
||||
addrCommentMap map[[ripemd160.Size]byte]*[]byte
|
||||
chainIdxMap map[int64]*[ripemd160.Size]byte
|
||||
txCommentMap map[[sha256.Size]byte]*[]byte
|
||||
lastChainIdx int64
|
||||
}
|
||||
|
||||
// WriteTo serializes a Wallet and writes it to a io.Writer,
|
||||
// returning the number of bytes written and any errors encountered.
|
||||
func (wallet *Wallet) WriteTo(w io.Writer) (n int64, err error) {
|
||||
// Iterate through each entry needing to be written. If data
|
||||
// implements io.WriterTo, use its WriteTo func. Otherwise,
|
||||
// data is a pointer to a fixed size value.
|
||||
datas := []interface{}{
|
||||
&wallet.fileID,
|
||||
&wallet.version,
|
||||
&wallet.netMagicBytes,
|
||||
&wallet.walletFlags,
|
||||
&wallet.uniqID,
|
||||
&wallet.createDate,
|
||||
&wallet.name,
|
||||
&wallet.description,
|
||||
&wallet.highestUsed,
|
||||
&wallet.kdfParams,
|
||||
&wallet.encryptionParams,
|
||||
&wallet.keyGenerator,
|
||||
make([]byte, 1024),
|
||||
&wallet.appendedEntries,
|
||||
}
|
||||
var read int64
|
||||
for _, data := range datas {
|
||||
if s, ok := data.(io.WriterTo); ok {
|
||||
read, err = s.WriteTo(w)
|
||||
} else {
|
||||
read, err = binaryWrite(w, binary.LittleEndian, data)
|
||||
}
|
||||
n += read
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// ReadFrom reads data from a io.Reader and saves it to a Wallet,
|
||||
// returning the number of bytes read and any errors encountered.
|
||||
func (wallet *Wallet) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
var read int64
|
||||
|
||||
wallet.addrMap = make(map[[ripemd160.Size]byte]*btcAddress)
|
||||
wallet.addrCommentMap = make(map[[ripemd160.Size]byte]*[]byte)
|
||||
wallet.chainIdxMap = make(map[int64]*[ripemd160.Size]byte)
|
||||
wallet.txCommentMap = make(map[[sha256.Size]byte]*[]byte)
|
||||
|
||||
// Iterate through each entry needing to be read. If data
|
||||
// implements io.ReaderFrom, use its ReadFrom func. Otherwise,
|
||||
// data is a pointer to a fixed sized value.
|
||||
datas := []interface{}{
|
||||
&wallet.fileID,
|
||||
&wallet.version,
|
||||
&wallet.netMagicBytes,
|
||||
&wallet.walletFlags,
|
||||
&wallet.uniqID,
|
||||
&wallet.createDate,
|
||||
&wallet.name,
|
||||
&wallet.description,
|
||||
&wallet.highestUsed,
|
||||
&wallet.kdfParams,
|
||||
&wallet.encryptionParams,
|
||||
&wallet.keyGenerator,
|
||||
make([]byte, 1024),
|
||||
&wallet.appendedEntries,
|
||||
}
|
||||
for _, data := range datas {
|
||||
var err error
|
||||
if rf, ok := data.(io.ReaderFrom); ok {
|
||||
read, err = rf.ReadFrom(r)
|
||||
} else {
|
||||
read, err = binaryRead(r, binary.LittleEndian, data)
|
||||
}
|
||||
n += read
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
// Add root address to address map
|
||||
wallet.addrMap[wallet.keyGenerator.pubKeyHash] = &wallet.keyGenerator
|
||||
wallet.chainIdxMap[wallet.keyGenerator.chainIndex] = &wallet.keyGenerator.pubKeyHash
|
||||
|
||||
// Fill unserializied fields.
|
||||
wts := ([]io.WriterTo)(wallet.appendedEntries)
|
||||
for _, wt := range wts {
|
||||
switch wt.(type) {
|
||||
case *addrEntry:
|
||||
e := wt.(*addrEntry)
|
||||
wallet.addrMap[e.pubKeyHash160] = &e.addr
|
||||
wallet.chainIdxMap[e.addr.chainIndex] = &e.pubKeyHash160
|
||||
if wallet.lastChainIdx < e.addr.chainIndex {
|
||||
wallet.lastChainIdx = e.addr.chainIndex
|
||||
}
|
||||
case *addrCommentEntry:
|
||||
e := wt.(*addrCommentEntry)
|
||||
wallet.addrCommentMap[e.pubKeyHash160] = &e.comment
|
||||
case *txCommentEntry:
|
||||
e := wt.(*txCommentEntry)
|
||||
wallet.txCommentMap[e.txHash] = &e.comment
|
||||
default:
|
||||
return n, errors.New("Unknown appended entry")
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Unlock derives an AES key from passphrase and wallet's KDF
|
||||
// parameters and unlocks the root key of the wallet.
|
||||
func (wallet *Wallet) Unlock(passphrase []byte) error {
|
||||
key := Key(passphrase, wallet.kdfParams.salt[:],
|
||||
wallet.kdfParams.mem, wallet.kdfParams.nIter)
|
||||
|
||||
// Attempt unlocking root address
|
||||
return wallet.keyGenerator.unlock(key)
|
||||
}
|
||||
|
||||
// Lock does a best effort to zero the keys.
|
||||
// Being go this might not succeed but try anway.
|
||||
// TODO(jrick)
|
||||
func (wallet *Wallet) Lock() {
|
||||
}
|
||||
|
||||
// Returns wallet version as string and int.
|
||||
// TODO(jrick)
|
||||
func (wallet *Wallet) Version() (string, int) {
|
||||
return "", 0
|
||||
}
|
||||
|
||||
// TODO(jrick)
|
||||
func (wallet *Wallet) NextUnusedAddress() string {
|
||||
_ = wallet.lastChainIdx
|
||||
wallet.highestUsed++
|
||||
new160, err := wallet.addr160ForIdx(wallet.highestUsed)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
addr := wallet.addrMap[*new160]
|
||||
if addr != nil {
|
||||
return btcutil.Base58Encode(addr.pubKeyHash[:])
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (wallet *Wallet) addr160ForIdx(idx int64) (*[ripemd160.Size]byte, error) {
|
||||
if idx > wallet.lastChainIdx {
|
||||
return nil, errors.New("Chain index out of range")
|
||||
}
|
||||
return wallet.chainIdxMap[idx], nil
|
||||
}
|
||||
|
||||
func (wallet *Wallet) GetActiveAddresses() []string {
|
||||
addrs := []string{}
|
||||
for i := int64(-1); i <= wallet.highestUsed; i++ {
|
||||
addr160, err := wallet.addr160ForIdx(i)
|
||||
if err != nil {
|
||||
return addrs
|
||||
}
|
||||
addr := wallet.addrMap[*addr160]
|
||||
addrs = append(addrs, btcutil.Base58Encode(addr.pubKeyHash[:]))
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
/*
|
||||
func OpenWallet(file string) (*Wallet, error) {
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
type btcAddress struct {
|
||||
pubKeyHash [ripemd160.Size]byte
|
||||
version uint32
|
||||
flags uint64
|
||||
chaincode [32]byte
|
||||
chainIndex int64
|
||||
chainDepth int64
|
||||
initVector [16]byte
|
||||
privKey [32]byte
|
||||
pubKey [65]byte
|
||||
firstSeen uint64
|
||||
lastSeen uint64
|
||||
firstBlock uint32
|
||||
lastBlock uint32
|
||||
privKeyCT []byte // Points to clear text private key if unlocked.
|
||||
}
|
||||
|
||||
// ReadFrom reads an encrypted address from an io.Reader.
|
||||
func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
var read int64
|
||||
|
||||
// Checksums
|
||||
var chkPubKeyHash uint32
|
||||
var chkChaincode uint32
|
||||
var chkInitVector uint32
|
||||
var chkPrivKey uint32
|
||||
var chkPubKey uint32
|
||||
|
||||
// Read serialized wallet into addr fields and checksums.
|
||||
datas := []interface{}{
|
||||
&addr.pubKeyHash,
|
||||
&chkPubKeyHash,
|
||||
&addr.version,
|
||||
&addr.flags,
|
||||
&addr.chaincode,
|
||||
&chkChaincode,
|
||||
&addr.chainIndex,
|
||||
&addr.chainDepth,
|
||||
&addr.initVector,
|
||||
&chkInitVector,
|
||||
&addr.privKey,
|
||||
&chkPrivKey,
|
||||
&addr.pubKey,
|
||||
&chkPubKey,
|
||||
&addr.firstSeen,
|
||||
&addr.lastSeen,
|
||||
&addr.firstBlock,
|
||||
&addr.lastBlock,
|
||||
}
|
||||
for _, data := range datas {
|
||||
if read, err = binaryRead(r, binary.LittleEndian, data); err != nil {
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
}
|
||||
|
||||
// Verify checksums, correct errors where possible.
|
||||
checks := []struct {
|
||||
data []byte
|
||||
chk uint32
|
||||
}{
|
||||
{addr.pubKeyHash[:], chkPubKeyHash},
|
||||
{addr.chaincode[:], chkChaincode},
|
||||
{addr.initVector[:], chkInitVector},
|
||||
{addr.privKey[:], chkPrivKey},
|
||||
{addr.pubKey[:], chkPubKey},
|
||||
}
|
||||
for i, _ := range checks {
|
||||
if err = verifyAndFix(checks[i].data, checks[i].chk); err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(jrick) verify encryption
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (addr *btcAddress) WriteTo(w io.Writer) (n int64, err error) {
|
||||
var written int64
|
||||
|
||||
datas := []interface{}{
|
||||
&addr.pubKeyHash,
|
||||
walletHash(addr.pubKeyHash[:]),
|
||||
&addr.version,
|
||||
&addr.flags,
|
||||
&addr.chaincode,
|
||||
walletHash(addr.chaincode[:]),
|
||||
&addr.chainIndex,
|
||||
&addr.chainDepth,
|
||||
&addr.initVector,
|
||||
walletHash(addr.initVector[:]),
|
||||
&addr.privKey,
|
||||
walletHash(addr.privKey[:]),
|
||||
&addr.pubKey,
|
||||
walletHash(addr.pubKey[:]),
|
||||
&addr.firstSeen,
|
||||
&addr.lastSeen,
|
||||
&addr.firstBlock,
|
||||
&addr.lastBlock,
|
||||
}
|
||||
for _, data := range datas {
|
||||
written, err = binaryWrite(w, binary.LittleEndian, data)
|
||||
if err != nil {
|
||||
return n + written, err
|
||||
}
|
||||
n += written
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (addr *btcAddress) unlock(key []byte) error {
|
||||
aesBlockDecrypter, err := aes.NewCipher([]byte(key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
aesDecrypter := cipher.NewCFBDecrypter(aesBlockDecrypter, addr.initVector[:])
|
||||
ct := make([]byte, 32)
|
||||
aesDecrypter.XORKeyStream(ct, addr.privKey[:])
|
||||
addr.privKeyCT = ct
|
||||
|
||||
pubKey, err := btcec.ParsePubKey(addr.pubKey[:], btcec.S256())
|
||||
if err != nil {
|
||||
return fmt.Errorf("ParsePubKey faild:", err)
|
||||
}
|
||||
x, y := btcec.S256().ScalarBaseMult(addr.privKeyCT)
|
||||
if x.Cmp(pubKey.X) != 0 || y.Cmp(pubKey.Y) != 0 {
|
||||
return fmt.Errorf("decryption failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(jrick)
|
||||
func (addr *btcAddress) changeEncryptionKey(oldkey, newkey []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(jrick)
|
||||
func (addr *btcAddress) verifyEncryptionKey() {
|
||||
}
|
||||
|
||||
// TODO(jrick)
|
||||
func newRandomAddress(key []byte) *btcAddress {
|
||||
addr := &btcAddress{}
|
||||
return addr
|
||||
}
|
||||
|
||||
func walletHash(b []byte) uint32 {
|
||||
sum := btcwire.DoubleSha256(b)
|
||||
return binary.LittleEndian.Uint32(sum)
|
||||
}
|
||||
|
||||
// TODO(jrick) add error correction.
|
||||
func verifyAndFix(b []byte, chk uint32) error {
|
||||
if walletHash(b) != chk {
|
||||
return ChecksumErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type kdfParameters struct {
|
||||
mem uint64
|
||||
nIter uint32
|
||||
salt [32]byte
|
||||
}
|
||||
|
||||
func (params *kdfParameters) WriteTo(w io.Writer) (n int64, err error) {
|
||||
var written int64
|
||||
|
||||
memBytes := make([]byte, 8)
|
||||
nIterBytes := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint64(memBytes, params.mem)
|
||||
binary.LittleEndian.PutUint32(nIterBytes, params.nIter)
|
||||
chkedBytes := append(memBytes, nIterBytes...)
|
||||
chkedBytes = append(chkedBytes, params.salt[:]...)
|
||||
|
||||
datas := []interface{}{
|
||||
¶ms.mem,
|
||||
¶ms.nIter,
|
||||
¶ms.salt,
|
||||
walletHash(chkedBytes),
|
||||
make([]byte, 256-(binary.Size(params)+4)), // padding
|
||||
}
|
||||
for _, data := range datas {
|
||||
if written, err = binaryWrite(w, binary.LittleEndian, data); err != nil {
|
||||
return n + written, err
|
||||
}
|
||||
n += written
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (params *kdfParameters) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
var read int64
|
||||
|
||||
// These must be read in but are not saved directly to params.
|
||||
chkedBytes := make([]byte, 44)
|
||||
var chk uint32
|
||||
padding := make([]byte, 256-(binary.Size(params)+4))
|
||||
|
||||
datas := []interface{}{
|
||||
chkedBytes,
|
||||
&chk,
|
||||
padding,
|
||||
}
|
||||
for _, data := range datas {
|
||||
if read, err = binaryRead(r, binary.LittleEndian, data); err != nil {
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
}
|
||||
|
||||
// Verify checksum
|
||||
if err = verifyAndFix(chkedBytes, chk); err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Write params
|
||||
buf := bytes.NewBuffer(chkedBytes)
|
||||
datas = []interface{}{
|
||||
¶ms.mem,
|
||||
¶ms.nIter,
|
||||
¶ms.salt,
|
||||
}
|
||||
for _, data := range datas {
|
||||
if err = binary.Read(buf, binary.LittleEndian, data); err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
type addrEntry struct {
|
||||
pubKeyHash160 [ripemd160.Size]byte
|
||||
addr btcAddress
|
||||
}
|
||||
|
||||
func (e *addrEntry) WriteTo(w io.Writer) (n int64, err error) {
|
||||
var written int64
|
||||
|
||||
// Write header
|
||||
if written, err = binaryWrite(w, binary.LittleEndian, addrHeader); err != nil {
|
||||
return n + written, err
|
||||
}
|
||||
n += written
|
||||
|
||||
// Write hash
|
||||
if written, err = binaryWrite(w, binary.LittleEndian, &e.pubKeyHash160); err != nil {
|
||||
return n + written, err
|
||||
}
|
||||
n += written
|
||||
|
||||
// Write btcAddress
|
||||
written, err = e.addr.WriteTo(w)
|
||||
return n + written, err
|
||||
}
|
||||
|
||||
func (e *addrEntry) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
var read int64
|
||||
|
||||
if read, err = binaryRead(r, binary.LittleEndian, &e.pubKeyHash160); err != nil {
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
|
||||
read, err = e.addr.ReadFrom(r)
|
||||
return n + read, err
|
||||
}
|
||||
|
||||
type addrCommentEntry struct {
|
||||
pubKeyHash160 [ripemd160.Size]byte
|
||||
comment []byte
|
||||
}
|
||||
|
||||
func (e *addrCommentEntry) WriteTo(w io.Writer) (n int64, err error) {
|
||||
var written int64
|
||||
|
||||
// Comments shall not overflow their entry.
|
||||
if len(e.comment) > maxCommentLen {
|
||||
return n, MalformedEntryErr
|
||||
}
|
||||
|
||||
// Write header
|
||||
if written, err = binaryWrite(w, binary.LittleEndian, addrCommentHeader); err != nil {
|
||||
return n + written, err
|
||||
}
|
||||
n += written
|
||||
|
||||
// Write hash
|
||||
if written, err = binaryWrite(w, binary.LittleEndian, &e.pubKeyHash160); err != nil {
|
||||
return n + written, err
|
||||
}
|
||||
n += written
|
||||
|
||||
// Write length
|
||||
if written, err = binaryWrite(w, binary.LittleEndian, uint16(len(e.comment))); err != nil {
|
||||
return n + written, err
|
||||
}
|
||||
n += written
|
||||
|
||||
// Write comment
|
||||
written, err = binaryWrite(w, binary.LittleEndian, e.comment)
|
||||
return n + written, err
|
||||
}
|
||||
|
||||
func (e *addrCommentEntry) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
var read int64
|
||||
|
||||
if read, err = binaryRead(r, binary.LittleEndian, &e.pubKeyHash160); err != nil {
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
|
||||
var clen uint16
|
||||
if read, err = binaryRead(r, binary.LittleEndian, &clen); err != nil {
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
|
||||
e.comment = make([]byte, clen)
|
||||
read, err = binaryRead(r, binary.LittleEndian, e.comment)
|
||||
return n + read, err
|
||||
}
|
||||
|
||||
type txCommentEntry struct {
|
||||
txHash [sha256.Size]byte
|
||||
comment []byte
|
||||
}
|
||||
|
||||
func (e *txCommentEntry) WriteTo(w io.Writer) (n int64, err error) {
|
||||
var written int64
|
||||
|
||||
// Comments shall not overflow their entry.
|
||||
if len(e.comment) > maxCommentLen {
|
||||
return n, MalformedEntryErr
|
||||
}
|
||||
|
||||
// Write header
|
||||
if written, err = binaryWrite(w, binary.LittleEndian, txCommentHeader); err != nil {
|
||||
return n + written, err
|
||||
}
|
||||
n += written
|
||||
|
||||
// Write length
|
||||
if written, err = binaryWrite(w, binary.LittleEndian, uint16(len(e.comment))); err != nil {
|
||||
return n + written, err
|
||||
}
|
||||
|
||||
// Write comment
|
||||
written, err = binaryWrite(w, binary.LittleEndian, e.comment)
|
||||
return n + written, err
|
||||
}
|
||||
|
||||
func (e *txCommentEntry) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
var read int64
|
||||
|
||||
if read, err = binaryRead(r, binary.LittleEndian, &e.txHash); err != nil {
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
|
||||
var clen uint16
|
||||
if read, err = binaryRead(r, binary.LittleEndian, &clen); err != nil {
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
|
||||
e.comment = make([]byte, clen)
|
||||
read, err = binaryRead(r, binary.LittleEndian, e.comment)
|
||||
return n + read, err
|
||||
}
|
||||
|
||||
type deletedEntry struct {
|
||||
}
|
||||
|
||||
func (e *deletedEntry) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
var read int64
|
||||
|
||||
var ulen uint16
|
||||
if read, err = binaryRead(r, binary.LittleEndian, &ulen); err != nil {
|
||||
return n + read, err
|
||||
}
|
||||
n += read
|
||||
|
||||
unused := make([]byte, ulen)
|
||||
if nRead, err := r.Read(unused); err == io.EOF {
|
||||
return n + int64(nRead), nil
|
||||
} else {
|
||||
return n + int64(nRead), err
|
||||
}
|
||||
}
|
||||
|
||||
type UTXOStore struct {
|
||||
}
|
||||
|
||||
type utxo struct {
|
||||
pubKeyHash [ripemd160.Size]byte
|
||||
*btcwire.TxOut
|
||||
block int64
|
||||
}
|
60
wallet/wallet_test.go
Normal file
60
wallet/wallet_test.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package wallet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBtcAddressSerializer(t *testing.T) {
|
||||
var addr = btcAddress{
|
||||
pubKeyHash: [20]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19},
|
||||
}
|
||||
|
||||
file, err := os.Create("btcaddress.bin")
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := addr.WriteTo(file); err != nil {
|
||||
t.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
file.Seek(0, 0)
|
||||
|
||||
var readAddr btcAddress
|
||||
_, err = readAddr.ReadFrom(file)
|
||||
if err != nil {
|
||||
spew.Dump(&readAddr)
|
||||
t.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
buf1, buf2 := new(bytes.Buffer), new(bytes.Buffer)
|
||||
binary.Write(buf1, binary.LittleEndian, addr)
|
||||
binary.Write(buf2, binary.LittleEndian, readAddr)
|
||||
if !bytes.Equal(buf1.Bytes(), buf2.Bytes()) {
|
||||
t.Error("Original and read btcAddress differ.")
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue