Ori Newman 4658f9d05c
Implement BIP 39 and HD wallet features (#1705)
* Naive bip39 with address reuse

* Avoid address reuse in libkaspawallet

* Add wallet daemon

* Use daemon everywhere

* Add forceOverride

* Make CreateUnsignedTransaction endpoint receive amount in sompis

* Collect close UTXOs

* Filter out non-spendable UTXOs from selectUTXOs

* Use different paths for multisig and non multisig

* Fix tests to use non zero path

* Fix multisig cosigner index detection

* Add comments

* Fix dump_unencrypted_data.go according to bip39 and bip32

* Fix wrong derivation path for multisig on wallet creation

* Remove IsSynced endpoint and add validation if wallet is synced for the relevant endpoints

* Rename server address to daemon address

* Fix capacity for extendedPublicKeys

* Use ReadBytes instead of ReadLine

* Add validation when importing

* Increment before using index value, and use it as is

* Save keys file exactly where needed

* Use %+v printErrorAndExit

* Remove redundant consts

* Rnemae collectCloseUTXOs and collectFarUTXOs

* Move typedefs around

* Add comment to addressesToQuery

* Update collectUTXOsFromRecentAddresses comment about locks

* Split collectUTXOs to small functions

* Add sanity check

* Add addEntryToUTXOSet function

* Change validateIsSynced to isSynced

* Simplify createKeyPairsFromFunction logic

* Rename .Sync() to .Save()

* Fix typo

* Create bip39BitSize const

* Add consts to purposes

* Add multisig check for 'send'

* Rename updatedPSTxBytes to partiallySignedTransaction

* Change collectUTXOsFromFarAddresses's comment

* Use setters for last used indexes

* Don't use the pstx acronym

* Fix SetPath

* Remove spaces when reading lines

* Fix walletserver to daemonaddress

* Fix isUTXOSpendable to use DAA score

Co-authored-by: Svarog <feanorr@gmail.com>
2021-05-19 10:03:23 +03:00

264 lines
6.5 KiB
Go

package server
import (
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/pkg/errors"
"time"
)
const (
// externalKeychain is the key chain that is used to create receive addresses
externalKeychain = 0
// internalKeychain is used to create change addresses
internalKeychain = 1
)
var keyChains = []uint8{externalKeychain, internalKeychain}
type walletAddressSet map[string]*walletAddress
func (was walletAddressSet) strings() []string {
addresses := make([]string, 0, len(was))
for addr := range was {
addresses = append(addresses, addr)
}
return addresses
}
func (s *server) sync() error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
err := s.collectUTXOsFromRecentAddresses()
if err != nil {
return err
}
err = s.collectUTXOsFromFarAddresses()
if err != nil {
return err
}
err = s.refreshExistingUTXOsWithLock()
if err != nil {
return err
}
}
return nil
}
const numIndexesToQuery = 100
// addressesToQuery scans the addresses in the given range. Because
// each cosigner in a multisig has its own unique path for generating
// addresses it goes over all the cosigners and add their addresses
// for each key chain.
func (s *server) addressesToQuery(start, end uint32) (walletAddressSet, error) {
addresses := make(walletAddressSet)
for index := start; index < end; index++ {
for cosignerIndex := uint32(0); cosignerIndex < uint32(len(s.keysFile.ExtendedPublicKeys)); cosignerIndex++ {
for _, keychain := range keyChains {
address := &walletAddress{
index: index,
cosignerIndex: cosignerIndex,
keyChain: keychain,
}
addressString, err := s.walletAddressString(address)
if err != nil {
return nil, err
}
addresses[addressString] = address
}
}
}
return addresses, nil
}
// collectUTXOsFromFarAddresses collects numIndexesToQuery UTXOs
// from the last point it stopped in the previous call.
func (s *server) collectUTXOsFromFarAddresses() error {
s.lock.Lock()
defer s.lock.Unlock()
err := s.collectUTXOs(s.nextSyncStartIndex, s.nextSyncStartIndex+numIndexesToQuery)
if err != nil {
return err
}
s.nextSyncStartIndex += numIndexesToQuery
return nil
}
func (s *server) maxUsedIndex() uint32 {
s.lock.RLock()
defer s.lock.RUnlock()
maxUsedIndex := s.keysFile.LastUsedExternalIndex()
if s.keysFile.LastUsedInternalIndex() > maxUsedIndex {
maxUsedIndex = s.keysFile.LastUsedInternalIndex()
}
return maxUsedIndex
}
// collectUTXOsFromRecentAddresses collects UTXOs from used addresses until
// the address with the index of the last used address + 1000.
// collectUTXOsFromRecentAddresses scans addresses in batches of numIndexesToQuery,
// and releases the lock between scans.
func (s *server) collectUTXOsFromRecentAddresses() error {
maxUsedIndex := s.maxUsedIndex()
for i := uint32(0); i < maxUsedIndex+1000; i += numIndexesToQuery {
err := s.collectUTXOsWithLock(i, i+numIndexesToQuery)
if err != nil {
return err
}
}
return nil
}
func (s *server) collectUTXOsWithLock(start, end uint32) error {
s.lock.Lock()
defer s.lock.Unlock()
return s.collectUTXOs(start, end)
}
func (s *server) collectUTXOs(start, end uint32) error {
addressSet, err := s.addressesToQuery(start, end)
if err != nil {
return err
}
getUTXOsByAddressesResponse, err := s.rpcClient.GetUTXOsByAddresses(addressSet.strings())
if err != nil {
return err
}
err = s.updateLastUsedIndexes(addressSet, getUTXOsByAddressesResponse)
if err != nil {
return err
}
err = s.updateUTXOs(addressSet, getUTXOsByAddressesResponse)
if err != nil {
return err
}
return nil
}
func (s *server) updateUTXOs(addressSet walletAddressSet,
getUTXOsByAddressesResponse *appmessage.GetUTXOsByAddressesResponseMessage) error {
for _, entry := range getUTXOsByAddressesResponse.Entries {
err := s.addEntryToUTXOSet(entry, addressSet)
if err != nil {
return err
}
}
return nil
}
func (s *server) updateLastUsedIndexes(addressSet walletAddressSet,
getUTXOsByAddressesResponse *appmessage.GetUTXOsByAddressesResponseMessage) error {
lastUsedExternalIndex := s.keysFile.LastUsedExternalIndex()
lastUsedInternalIndex := s.keysFile.LastUsedInternalIndex()
for _, entry := range getUTXOsByAddressesResponse.Entries {
walletAddress, ok := addressSet[entry.Address]
if !ok {
return errors.Errorf("Got result from address %s even though it wasn't requested", entry.Address)
}
if walletAddress.cosignerIndex != s.keysFile.CosignerIndex {
continue
}
if walletAddress.keyChain == externalKeychain {
if walletAddress.index > lastUsedExternalIndex {
lastUsedExternalIndex = walletAddress.index
}
continue
}
if walletAddress.index > lastUsedInternalIndex {
lastUsedInternalIndex = walletAddress.index
}
}
err := s.keysFile.SetLastUsedExternalIndex(lastUsedExternalIndex)
if err != nil {
return err
}
return s.keysFile.SetLastUsedInternalIndex(lastUsedInternalIndex)
}
func (s *server) refreshExistingUTXOsWithLock() error {
s.lock.Lock()
defer s.lock.Unlock()
return s.refreshExistingUTXOs()
}
func (s *server) addEntryToUTXOSet(entry *appmessage.UTXOsByAddressesEntry, addressSet walletAddressSet) error {
outpoint, err := appmessage.RPCOutpointToDomainOutpoint(entry.Outpoint)
if err != nil {
return err
}
utxoEntry, err := appmessage.RPCUTXOEntryToUTXOEntry(entry.UTXOEntry)
if err != nil {
return err
}
address, ok := addressSet[entry.Address]
if !ok {
return errors.Errorf("Got result from address %s even though it wasn't requested", entry.Address)
}
s.utxos[*outpoint] = &walletUTXO{
Outpoint: outpoint,
UTXOEntry: utxoEntry,
address: address,
}
return nil
}
func (s *server) refreshExistingUTXOs() error {
addressSet := make(walletAddressSet, len(s.utxos))
for _, utxo := range s.utxos {
addressString, err := s.walletAddressString(utxo.address)
if err != nil {
return err
}
addressSet[addressString] = utxo.address
}
getUTXOsByAddressesResponse, err := s.rpcClient.GetUTXOsByAddresses(addressSet.strings())
if err != nil {
return err
}
s.utxos = make(map[externalapi.DomainOutpoint]*walletUTXO, len(getUTXOsByAddressesResponse.Entries))
for _, entry := range getUTXOsByAddressesResponse.Entries {
err := s.addEntryToUTXOSet(entry, addressSet)
if err != nil {
return err
}
}
return nil
}
func (s *server) isSynced() bool {
return s.nextSyncStartIndex > s.keysFile.LastUsedInternalIndex() && s.nextSyncStartIndex > s.keysFile.LastUsedExternalIndex()
}