You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

362 lines
13 KiB

// Copyright 2017-2018 DERO Project. All rights reserved.
// Use of this source code in any form is governed by RESEARCH license.
// license can be found in the LICENSE file.
// GPG: 0F39 E425 8C65 3947 702A 8234 08B2 0360 A03A 9DE8
//
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package walletapi
import "fmt"
import "sync"
import "github.com/arnaucode/derosuite/config"
import "github.com/arnaucode/derosuite/crypto"
import "github.com/arnaucode/derosuite/crypto/ringct"
import "github.com/arnaucode/derosuite/globals"
import "github.com/arnaucode/derosuite/walletapi/mnemonics"
import "github.com/arnaucode/derosuite/address"
import "github.com/arnaucode/derosuite/blockchain/inputmaturity"
type _Keys struct {
Spendkey_Secret crypto.Key
Spendkey_Public crypto.Key
Viewkey_Secret crypto.Key
Viewkey_Public crypto.Key
}
type Account struct {
Keys _Keys
SeedLanguage string
ViewOnly bool // is this viewonly wallet
Index_Global uint64 // till where the indexes have been processed
Height uint64
Balance uint64 // total balance of account
Balance_Locked uint64 // balance locked
Outputs_Array []TX_Wallet_Data // all outputs found in the chain belonging to us, as found in chain
// uint64 si the Index_Global which is the unique number
Outputs_Index map[uint64]bool // all outputs which are ours for deduplication
Outputs_Ready map[uint64]TX_Wallet_Data // these outputs are ready for consumption ( maturity needs to be checked)
Keyimages_Ready map[crypto.Key]bool // keyimages which are ready to get consumed, // we monitor them to find which
Outputs_Consumed map[crypto.Key]TX_Wallet_Data // the key is the keyimage
sync.Mutex // syncronise modifications to this structure
}
// this structure is kept by wallet
type TX_Wallet_Data struct {
TXdata globals.TX_Output_Data // all the fields of output data
WAmount uint64 // actual amount, in case of miner it is verbatim, for other cases it decrypted
WKey ringct.CtKey // key which is used to later send this specific output
WKimage crypto.Key // key image which gets consumed when this output is spent
WSpent bool // whether this output has been spent
}
// generate keys from using random numbers
func Generate_Keys_From_Random() (user *Account, err error) {
user = &Account{}
seed := crypto.RandomScalar()
user.Keys = Generate_Keys_From_Seed(*seed)
// initialize maps now
user.Outputs_Index = map[uint64]bool{}
user.Outputs_Ready = map[uint64]TX_Wallet_Data{}
user.Outputs_Consumed = map[crypto.Key]TX_Wallet_Data{}
user.Keyimages_Ready = map[crypto.Key]bool{}
return
}
// generate keys from seed which is from the recovery words
// or we feed in direct
func Generate_Keys_From_Seed(Seed crypto.Key) (keys _Keys) {
// setup main keys
keys.Spendkey_Secret = Seed
keys.Spendkey_Public = *(Seed.PublicKey())
// view keys are generated from secret ( so as single recovery seed is enough )
hash := crypto.Key(crypto.Keccak256(Seed[:]))
crypto.ScReduce32(&hash)
keys.Viewkey_Secret = hash
keys.Viewkey_Public = *(keys.Viewkey_Secret.PublicKey())
return
}
// generate user account using recovery seeds
func Generate_Account_From_Recovery_Words(words string) (user *Account, err error) {
user = &Account{}
language, seed, err := mnemonics.Words_To_Key(words)
if err != nil {
return
}
user.SeedLanguage = language
user.Keys = Generate_Keys_From_Seed(seed)
// initialize maps now
user.Outputs_Index = map[uint64]bool{}
user.Outputs_Ready = map[uint64]TX_Wallet_Data{}
user.Outputs_Consumed = map[crypto.Key]TX_Wallet_Data{}
user.Keyimages_Ready = map[crypto.Key]bool{}
return
}
func Generate_Account_From_Seed(Seed crypto.Key) (user *Account, err error) {
user = &Account{}
// TODO check whether the seed is invalid
user.Keys = Generate_Keys_From_Seed(Seed)
// initialize maps now
user.Outputs_Index = map[uint64]bool{}
user.Outputs_Ready = map[uint64]TX_Wallet_Data{}
user.Outputs_Consumed = map[crypto.Key]TX_Wallet_Data{}
user.Keyimages_Ready = map[crypto.Key]bool{}
return
}
// generate keys for view only wallet
func Generate_Account_View_Only(Publicspend crypto.Key, ViewSecret crypto.Key) (user *Account, err error) {
user = &Account{}
// TODO check whether seed is valid secret
user.Keys.Spendkey_Public = Publicspend
user.Keys.Viewkey_Secret = ViewSecret
user.Keys.Viewkey_Public = *(ViewSecret.PublicKey())
user.ViewOnly = true
// initialize maps
user.Outputs_Index = map[uint64]bool{}
user.Outputs_Ready = map[uint64]TX_Wallet_Data{}
user.Outputs_Consumed = map[crypto.Key]TX_Wallet_Data{}
user.Keyimages_Ready = map[crypto.Key]bool{}
return
}
// convert key to seed using language
func (user *Account) GetSeed() (str string) {
return mnemonics.Key_To_Words(user.Keys.Spendkey_Secret, user.SeedLanguage)
}
// view wallet key consists of public spendkey and private view key
func (user *Account) GetViewWalletKey() (str string) {
return fmt.Sprintf("%s%s", user.Keys.Spendkey_Public, user.Keys.Viewkey_Secret)
}
// convert a user account to address
func (user *Account) GetAddress() (addr address.Address) {
switch globals.Config.Name {
case "testnet":
addr.Network = config.Testnet.Public_Address_Prefix //choose dETo
default:
fallthrough // assume mainnet
case "mainnet":
addr.Network = config.Mainnet.Public_Address_Prefix //choose dERo
//panic(fmt.Sprintf("Unknown Network \"%s\"", globals.Config.Name))
}
addr.SpendKey = user.Keys.Spendkey_Public
addr.ViewKey = user.Keys.Viewkey_Public
return
}
// one simple function which does all the crypto to find out whether output belongs to this account
// NOTE: this function only uses view key secret and Spendkey_Public
// output index is the position of vout within the tx list itself
func (user *Account) Is_Output_Ours(tx_public crypto.Key, output_index uint64, vout_key crypto.Key) bool {
derivation := crypto.KeyDerivation(&tx_public, &user.Keys.Viewkey_Secret)
derivation_public_key := derivation.KeyDerivation_To_PublicKey(output_index, user.Keys.Spendkey_Public)
return derivation_public_key == vout_key
}
// this function does all the keyderivation required for decrypting ringct outputs, generate keyimage etc
// also used when we build up a transaction for mining or sending amount
func (user *Account) Generate_Helper_Key_Image(tx_public crypto.Key, output_index uint64) (ephermal_secret, ephermal_public, keyimage crypto.Key) {
derivation := crypto.KeyDerivation(&tx_public, &user.Keys.Viewkey_Secret)
ephermal_public = derivation.KeyDerivation_To_PublicKey(output_index, user.Keys.Spendkey_Public)
ephermal_secret = derivation.KeyDerivation_To_PrivateKey(output_index, user.Keys.Spendkey_Secret)
keyimage = crypto.GenerateKeyImage(ephermal_public, ephermal_secret)
return
}
// this function decodes ringCT encoded output amounts
// this is only possible if signature is full or simple
func (user *Account) Decode_RingCT_Output(tx_public crypto.Key, output_index uint64, pkkey crypto.Key, tuple ringct.ECdhTuple, sigtype uint64) (amount uint64, mask ringct.Key, result bool) {
derivation := crypto.KeyDerivation(&tx_public, &user.Keys.Viewkey_Secret)
scalar_key := derivation.KeyDerivationToScalar(output_index)
switch sigtype {
case 0: // NOT possible , miner tx outputs are not hidden
return
case 1: // ringct MG // Both ringct outputs can be decoded using the same methods
// however, original implementation has different methods, maybe need to evaluate more
fallthrough
case 2: // ringct sample
amount, mask, result = ringct.Decode_Amount(tuple, ringct.Key(*scalar_key), ringct.Key(pkkey))
default:
return
}
return
}
// add the transaction to our wallet record, so as funds can be later on tracked
// due to enhanced features, we have to wait and watch for all funds
// this will extract secret keys from newly arrived funds to consume them later on
func (user *Account) Add_Transaction_Record_Funds(txdata *globals.TX_Output_Data) (result bool) {
user.Lock()
defer user.Unlock()
var tx_wallet TX_Wallet_Data
// confirm once again that data belongs to this user
if !user.Is_Output_Ours(txdata.Tx_Public_Key, txdata.Index_within_tx, crypto.Key(txdata.InKey.Destination)) {
return false // output is not ours
}
// setup Amount
switch txdata.SigType {
case 0: // miner tx
tx_wallet.WAmount = txdata.Amount
tx_wallet.WKey.Mask = ringct.ZeroCommitment_From_Amount(txdata.Amount)
case 1, 2: // ringct full/simple
tx_wallet.WAmount, tx_wallet.WKey.Mask, result = user.Decode_RingCT_Output(txdata.Tx_Public_Key, txdata.Index_within_tx, crypto.Key(txdata.InKey.Mask), txdata.ECDHTuple,
txdata.SigType)
if result == false { // It's an internal error most probably
return false
}
}
tx_wallet.TXdata = *txdata
// check whether we are deduplicating, is the transaction already in our records, skip it
if _, ok := user.Outputs_Index[txdata.Index_Global]; ok { // transaction is already in our wallet, skip it for being duplicate
return false
}
// if wallet is viewonly, we cannot track when the funds were spent
// so lets skip the part, since we do not have th keys
if !user.ViewOnly { // it's a full wallet, track spendable and get ready to spend
secret_key, _, kimage := user.Generate_Helper_Key_Image(txdata.Tx_Public_Key, txdata.Index_within_tx)
user.Keyimages_Ready[kimage] = true // monitor this key image for consumption
tx_wallet.WKimage = kimage
tx_wallet.WKey.Destination = ringct.Key(secret_key)
}
// add tx info to wallet
user.Outputs_Index[txdata.Index_Global] = true // deduplication it if it ever comes again
user.Outputs_Ready[txdata.Index_Global] = tx_wallet
user.Outputs_Array = append(user.Outputs_Array, tx_wallet)
return true
}
// check whether our fund is consumed
// this is done by finding the keyimages floating in blockchain, to what keyimages belong to this account
// if match is found, we have consumed our funds
func (user *Account) Is_Our_Fund_Consumed(key_image crypto.Key) (amount uint64, result bool) {
if _, ok := user.Keyimages_Ready[key_image]; ok {
user.Lock()
defer user.Unlock()
for k, _ := range user.Outputs_Ready {
if user.Outputs_Ready[k].WKimage == key_image {
return user.Outputs_Ready[k].WAmount, true // return ammount and success
}
}
fmt.Printf("This case should NOT be possible theoritically\n")
return 0, true
}
return 0, false
}
// add the transaction to record,
// this will mark the funds as consumed on the basis of keyimages
// locate the transaction and get the amount , this is O(n), so we can tell how much funds were spent
// cryptnote only allows to spend complete funds, change comes back
func (user *Account) Consume_Transaction_Record_Funds(txdata *globals.TX_Output_Data, key_image crypto.Key) bool {
var tx_wallet TX_Wallet_Data
if _, ok := user.Keyimages_Ready[key_image]; ok {
user.Lock()
defer user.Unlock()
for k, _ := range user.Outputs_Ready {
if user.Outputs_Ready[k].WKimage == key_image { // find the input corressponding to this image
// mark output as consumed, move it to consumed map, delete it from ready map
tx_wallet.TXdata = *txdata
tx_wallet.WAmount = user.Outputs_Ready[k].WAmount // take amount from original TX
tx_wallet.WSpent = true // mark this fund as spent
delete(user.Outputs_Ready, k)
user.Outputs_Consumed[key_image] = tx_wallet
user.Outputs_Array = append(user.Outputs_Array, user.Outputs_Consumed[key_image])
return true // return success
}
}
fmt.Printf("This case should NOT be possible theoritically\n")
// locate the transaction and get the amount , this is O(n)
return true
}
return false
}
// get the unlocked balance ( amounts which are mature and can be spent at this time )
// offline wallets may get this wrong, since they may not have latest data
// TODO: for offline wallets, we must make all balance as mature
func (user *Account) Get_Balance() (mature_balance uint64, locked_balance uint64) {
user.Lock()
defer user.Unlock()
for k := range user.Outputs_Ready {
if inputmaturity.Is_Input_Mature(user.Height,
user.Outputs_Ready[k].TXdata.Height,
user.Outputs_Ready[k].TXdata.Unlock_Height,
user.Outputs_Ready[k].TXdata.SigType) {
mature_balance += user.Outputs_Ready[k].WAmount
} else {
locked_balance += user.Outputs_Ready[k].WAmount
}
}
return
}