Commit 35668743 authored by David Vorick's avatar David Vorick Committed by Luke Champine

add test coverage for SendSiacoins

Also abstract the signing code for the transaction builder into a
single, more generic function.
parent 6ee0fd29
......@@ -65,6 +65,6 @@ func VerifyHash(data Hash, pk PublicKey, sig Signature) error {
// PublicKey returns the public key that corresponds to a secret key.
func (sk SecretKey) PublicKey() (pk PublicKey) {
copy(pk[:], sk[:32])
copy(pk[:], sk[32:])
return
}
......@@ -143,10 +143,7 @@ func New(gateway modules.Gateway, saveDir string) (*ConsensusSet, error) {
}
// Fill out the consensus information for the genesis block.
cs.siacoinOutputs[genesisBlock.MinerPayoutID(0)] = types.SiacoinOutput{
Value: types.CalculateCoinbase(0),
UnlockHash: types.ZeroUnlockHash,
}
cs.siacoinOutputs[genesisBlock.MinerPayoutID(0)] = types.SiacoinOutput{Value: types.CalculateCoinbase(0)}
// Create the consensus directory.
err := os.MkdirAll(saveDir, 0700)
......
......@@ -75,7 +75,7 @@ func (h *Host) considerTerms(terms modules.ContractTerms) error {
case len(terms.MissedProofOutputs) != 1:
return errors.New("refund len does not match host settings")
case terms.MissedProofOutputs[0].UnlockHash != types.ZeroUnlockHash:
case terms.MissedProofOutputs[0].UnlockHash != types.UnlockHash{}:
return errors.New("coins are not paying out to correct address")
}
......@@ -127,7 +127,7 @@ func verifyTransaction(txn types.Transaction, terms modules.ContractTerms, merkl
case fc.MissedProofOutputs[0].UnlockHash != terms.MissedProofOutputs[0].UnlockHash:
return errors.New("bad file contract missed proof outputs")
case fc.UnlockHash != types.ZeroUnlockHash:
case fc.UnlockHash != types.UnlockHash{}:
return errors.New("bad file contract termination hash")
}
return nil
......
......@@ -116,7 +116,7 @@ func (r *Renter) negotiateContract(host modules.HostSettings, up modules.FileUpl
},
MissedProofOutputs: []types.SiacoinOutput{
{Value: validOutputValue, UnlockHash: types.ZeroUnlockHash},
{Value: validOutputValue, UnlockHash: types.UnlockHash{}},
},
}
......
......@@ -10,11 +10,11 @@ import (
const (
WalletDir = "wallet"
PublicKeysPerSeed = 100
PublicKeysPerSeed = 10
)
var (
LowBalanceErr = errors.New("Insufficient Balance")
ErrLowBalance = errors.New("Insufficient Balance")
)
type (
......@@ -25,9 +25,6 @@ type (
// WalletTransactionID is a unique identifier for a wallet transaction.
WalletTransactionID crypto.Hash
// WalletTransaction contains the metadata of a single output that changed
// the balance of the wallet, either incoming or outgoing (which can be
// gleaned from the 'Source' and 'Destination'.
WalletTransaction struct {
TransactionID types.TransactionID
ConfirmationHeight types.BlockHeight
......
package wallet
import (
"github.com/NebulousLabs/Sia/build"
"github.com/NebulousLabs/Sia/types"
)
......@@ -28,33 +27,11 @@ func (w *Wallet) UnconfirmedBalance() (outgoingSiacoins types.Currency, incoming
lockID := w.mu.Lock()
defer w.mu.Unlock(lockID)
// Tally up all outgoing siacoins.
unconfirmedOutputs := make(map[types.SiacoinOutputID]types.SiacoinOutput) // helps where unconfirmed outputs have been spent.
for _, txn := range w.unconfirmedTransactions {
for _, sci := range txn.SiacoinInputs {
uh := sci.UnlockConditions.UnlockHash()
_, exists := w.keys[uh]
if exists {
sco, exists := w.siacoinOutputs[sci.ParentID]
if exists {
outgoingSiacoins = outgoingSiacoins.Add(sco.Value)
} else {
sco, exists = unconfirmedOutputs[sci.ParentID]
if exists {
outgoingSiacoins = outgoingSiacoins.Add(sco.Value)
} else if build.DEBUG {
panic("unconfirmed siacoin output not found, yet spent")
}
}
}
}
for i, sco := range txn.SiacoinOutputs {
scoid := txn.SiacoinOutputID(i)
_, exists := w.keys[sco.UnlockHash]
if exists {
incomingSiacoins = incomingSiacoins.Add(sco.Value)
}
unconfirmedOutputs[scoid] = sco
for _, uwt := range w.unconfirmedWalletTransactions {
if uwt.FundType == types.SpecifierSiacoinOutput {
incomingSiacoins = incomingSiacoins.Add(uwt.Value)
} else if uwt.FundType == types.SpecifierSiacoinInput {
outgoingSiacoins = outgoingSiacoins.Add(uwt.Value)
}
}
return
......
......@@ -176,6 +176,7 @@ func (w *Wallet) createSeed(masterKey crypto.TwofishKey) (modules.Seed, error) {
if err != nil {
return modules.Seed{}, err
}
w.primarySeed = seed
w.settings.PrimarySeedFile = SeedFile{sfuid, encryptionVerification, cryptSeed}
w.settings.PrimarySeedProgress = 0
w.settings.PrimarySeedFilename = seedFilePrefix + randomSuffix + seedFileSuffix
......
......@@ -2,7 +2,6 @@ package wallet
import (
"bytes"
"errors"
"github.com/NebulousLabs/Sia/build"
"github.com/NebulousLabs/Sia/crypto"
......@@ -10,10 +9,6 @@ import (
"github.com/NebulousLabs/Sia/types"
)
var (
ErrInvalidID = errors.New("no transaction of given id found")
)
type transactionBuilder struct {
parents []types.Transaction
transaction types.Transaction
......@@ -23,6 +18,48 @@ type transactionBuilder struct {
wallet *Wallet
}
// addSignatures will sign a transaction using a spendable key, with support
// for multisig spendable keys. Because of the restricted input, the function
// is compatible with both siacoin inputs and siafund inputs.
func addSignatures(txn *types.Transaction, cf types.CoveredFields, uc types.UnlockConditions, parentID crypto.Hash, key spendableKey) error {
usedIndices := make(map[int]struct{})
for i := range key.secretKeys {
found := false
keyIndex := 0
pubKey := key.secretKeys[i].PublicKey()
for i, siaPublicKey := range uc.PublicKeys {
_, exists := usedIndices[i]
if !exists && bytes.Compare(pubKey[:], siaPublicKey.Key) == 0 {
found = true
keyIndex = i
break
}
}
if !found && build.DEBUG {
panic("transaction builder cannot sign an input that it added")
}
usedIndices[keyIndex] = struct{}{}
// Create the unsigned transaction signature.
sig := types.TransactionSignature{
ParentID: parentID,
CoveredFields: cf,
PublicKeyIndex: uint64(keyIndex),
}
txn.TransactionSignatures = append(txn.TransactionSignatures, sig)
// Get the signature.
sigIndex := len(txn.TransactionSignatures) - 1
sigHash := txn.SigHash(sigIndex)
encodedSig, err := crypto.SignHash(sigHash, key.secretKeys[i])
if err != nil {
return err
}
txn.TransactionSignatures[sigIndex].Signature = encodedSig[:]
}
return nil
}
// RegisterTransaction takes a transaction and its parents and returns a
// TransactionBuilder which can be used to expand the transaction. The most
// typical call is 'RegisterTransaction(types.Transaction{}, nil)', which
......@@ -80,6 +117,9 @@ func (tb *transactionBuilder) FundSiacoins(amount types.Currency) error {
break
}
}
if fund.Cmp(amount) < 0 {
return modules.ErrLowBalance
}
// Create and add the output that will be used to fund the standard
// transaction.
......@@ -107,28 +147,11 @@ func (tb *transactionBuilder) FundSiacoins(amount types.Currency) error {
}
// Sign all of the inputs to the parent trancstion.
coveredFields := types.CoveredFields{WholeTransaction: true}
for _, input := range parentTxn.SiacoinInputs {
sig := types.TransactionSignature{
ParentID: crypto.Hash(input.ParentID),
CoveredFields: coveredFields,
PublicKeyIndex: 0,
}
parentTxn.TransactionSignatures = append(parentTxn.TransactionSignatures, sig)
// Hash the transaction according to the covered fields.
coinAddress := input.UnlockConditions.UnlockHash()
sigIndex := len(parentTxn.TransactionSignatures) - 1
secKey := tb.wallet.keys[coinAddress].secretKeys[0]
sigHash := parentTxn.SigHash(sigIndex)
// Get the signature.
var encodedSig crypto.Signature
encodedSig, err = crypto.SignHash(sigHash, secKey)
for _, sci := range parentTxn.SiacoinInputs {
err := addSignatures(&parentTxn, types.FullCoveredFields, sci.UnlockConditions, crypto.Hash(sci.ParentID), tb.wallet.keys[sci.UnlockConditions.UnlockHash()])
if err != nil {
return err
}
parentTxn.TransactionSignatures[sigIndex].Signature = encodedSig[:]
}
// Add the exact output.
......@@ -212,28 +235,11 @@ func (tb *transactionBuilder) FundSiafunds(amount types.Currency) error {
}
// Sign all of the inputs to the parent trancstion.
coveredFields := types.CoveredFields{WholeTransaction: true}
for _, input := range parentTxn.SiafundInputs {
sig := types.TransactionSignature{
ParentID: crypto.Hash(input.ParentID),
CoveredFields: coveredFields,
PublicKeyIndex: 0,
}
parentTxn.TransactionSignatures = append(parentTxn.TransactionSignatures, sig)
// Hash the transaction according to the covered fields.
fundAddress := input.UnlockConditions.UnlockHash()
sigIndex := len(parentTxn.TransactionSignatures) - 1
secKey := tb.wallet.keys[fundAddress].secretKeys[0]
sigHash := parentTxn.SigHash(sigIndex)
// Get the signature.
var encodedSig crypto.Signature
encodedSig, err = crypto.SignHash(sigHash, secKey)
for _, sfi := range parentTxn.SiafundInputs {
err := addSignatures(&parentTxn, types.FullCoveredFields, sfi.UnlockConditions, crypto.Hash(sfi.ParentID), tb.wallet.keys[sfi.UnlockConditions.UnlockHash()])
if err != nil {
return err
}
parentTxn.TransactionSignatures[sigIndex].Signature = encodedSig[:]
}
// Add the exact output.
......@@ -384,92 +390,18 @@ func (tb *transactionBuilder) Sign(wholeTransaction bool) ([]types.Transaction,
defer tb.wallet.mu.Unlock(lockID)
for _, inputIndex := range tb.siacoinInputs {
input := txn.SiacoinInputs[inputIndex]
spendableKey := tb.wallet.keys[input.UnlockConditions.UnlockHash()]
usedIndices := make(map[int]struct{})
// Must use 'i := range' to prevent a copy of the secret key from being
// made.
for i := range spendableKey.secretKeys {
// Find the public key index that corresponds to this key.
found := false
keyIndex := 0
pubKey := spendableKey.secretKeys[i].PublicKey()
for i, siaPublicKey := range input.UnlockConditions.PublicKeys {
_, exists := usedIndices[i]
if bytes.Compare(pubKey[:], siaPublicKey.Key) == 0 && exists {
found = true
keyIndex = i
break
}
}
if !found && build.DEBUG {
panic("transaction builder cannot sign an input that it added")
}
usedIndices[keyIndex] = struct{}{}
// Create the unsigned transaction signature.
input := txn.SiacoinInputs[inputIndex]
sig := types.TransactionSignature{
ParentID: crypto.Hash(input.ParentID),
CoveredFields: coveredFields,
PublicKeyIndex: uint64(keyIndex),
}
txn.TransactionSignatures = append(txn.TransactionSignatures, sig)
// Hash the transaction according to the covered fields.
sigIndex := len(txn.TransactionSignatures) - 1
sigHash := txn.SigHash(sigIndex)
// Get the signature.
encodedSig, err := crypto.SignHash(sigHash, spendableKey.secretKeys[i])
if err != nil {
return nil, err
}
txn.TransactionSignatures[sigIndex].Signature = encodedSig[:]
key := tb.wallet.keys[input.UnlockConditions.UnlockHash()]
err := addSignatures(&txn, coveredFields, input.UnlockConditions, crypto.Hash(input.ParentID), key)
if err != nil {
return nil, err
}
}
for _, inputIndex := range tb.siafundInputs {
input := txn.SiafundInputs[inputIndex]
spendableKey := tb.wallet.keys[input.UnlockConditions.UnlockHash()]
usedIndices := make(map[int]struct{})
// Must use 'i := range' to prevent copies of the secret data from
// being made.
for i := range spendableKey.secretKeys {
// Find the public key index that corresponds to this key.
found := false
keyIndex := 0
pubKey := spendableKey.secretKeys[i].PublicKey()
for i, siaPublicKey := range input.UnlockConditions.PublicKeys {
_, exists := usedIndices[i]
if bytes.Compare(pubKey[:], siaPublicKey.Key) == 0 && exists {
found = true
keyIndex = i
break
}
}
if !found && build.DEBUG {
panic("transaction builder cannot sign an input that it added")
}
usedIndices[keyIndex] = struct{}{}
// Create the unsigned transaction signature.
input := txn.SiafundInputs[inputIndex]
sig := types.TransactionSignature{
ParentID: crypto.Hash(input.ParentID),
CoveredFields: coveredFields,
PublicKeyIndex: uint64(keyIndex),
}
txn.TransactionSignatures = append(txn.TransactionSignatures, sig)
// Hash the transaction according to the covered fields.
sigIndex := len(txn.TransactionSignatures) - 1
sigHash := txn.SigHash(sigIndex)
// Get the signature.
encodedSig, err := crypto.SignHash(sigHash, spendableKey.secretKeys[i])
if err != nil {
return nil, err
}
txn.TransactionSignatures[sigIndex].Signature = encodedSig[:]
key := tb.wallet.keys[input.UnlockConditions.UnlockHash()]
err := addSignatures(&txn, coveredFields, input.UnlockConditions, crypto.Hash(input.ParentID), key)
if err != nil {
return nil, err
}
}
......
......@@ -83,7 +83,13 @@ func (w *Wallet) ProcessConsensusChange(cc modules.ConsensusChange) {
// Iterate through the output diffs (siacoin and siafund) and apply all of
// them. Only apply the outputs that relate to unlock hashes we understand.
for _, diff := range cc.SiacoinOutputDiffs {
_, exists := w.siacoinOutputs[diff.ID]
// Verify that the diff is relevant to the wallet.
_, exists := w.keys[diff.SiacoinOutput.UnlockHash]
if !exists {
continue
}
_, exists = w.siacoinOutputs[diff.ID]
if diff.Direction == modules.DiffApply {
if exists && build.DEBUG {
panic("adding an existing output to wallet")
......@@ -97,7 +103,13 @@ func (w *Wallet) ProcessConsensusChange(cc modules.ConsensusChange) {
}
}
for _, diff := range cc.SiafundOutputDiffs {
_, exists := w.siafundOutputs[diff.ID]
// Verify that the diff is relevant to the wallet.
_, exists := w.keys[diff.SiafundOutput.UnlockHash]
if !exists {
continue
}
_, exists = w.siafundOutputs[diff.ID]
if diff.Direction == modules.DiffApply {
if exists && build.DEBUG {
panic("adding an existing output to wallet")
......@@ -202,7 +214,7 @@ func (w *Wallet) ReceiveUpdatedUnconfirmedTransactions(txns []types.Transaction,
RelatedAddress: sci.UnlockConditions.UnlockHash(),
Value: w.historicOutputs[types.OutputID(sci.ParentID)],
}
w.unconfirmedWalletTransactions = append(w.walletTransactions, wt)
w.unconfirmedWalletTransactions = append(w.unconfirmedWalletTransactions, wt)
}
}
for i, sco := range txn.SiacoinOutputs {
......@@ -219,7 +231,7 @@ func (w *Wallet) ReceiveUpdatedUnconfirmedTransactions(txns []types.Transaction,
RelatedAddress: sco.UnlockHash,
Value: sco.Value,
}
w.unconfirmedWalletTransactions = append(w.walletTransactions, wt)
w.unconfirmedWalletTransactions = append(w.unconfirmedWalletTransactions, wt)
oid := types.OutputID(txn.SiacoinOutputID(i))
w.historicOutputs[oid] = sco.Value
}
......
......@@ -11,11 +11,6 @@ import (
)
const (
// TransactionFee is yet another deprecated-on-arrival constant that says
// how large the transaction fees should be. This should really be a
// function supplied by the transaction pool.
TransactionFee = 10
// RespendTimeout records the number of blocks that the wallet will wait
// before spending an output that has been spent in the past. If the
// transaction spending the output has not made it to the transaction pool
......@@ -40,11 +35,10 @@ type Wallet struct {
settings WalletSettings
primarySeed modules.Seed
state modules.ConsensusSet
tpool modules.TransactionPool
consensusSetHeight types.BlockHeight
siafundPool types.Currency
unconfirmedTransactions []types.Transaction
state modules.ConsensusSet
tpool modules.TransactionPool
consensusSetHeight types.BlockHeight
siafundPool types.Currency
keys map[types.UnlockHash]spendableKey
siacoinOutputs map[types.SiacoinOutputID]types.SiacoinOutput
......@@ -126,7 +120,7 @@ func (w *Wallet) Close() error {
// SendSiacoins creates a transaction sending 'amount' to 'dest'. The transaction
// is submitted to the transaction pool and is also returned.
func (w *Wallet) SendSiacoins(amount types.Currency, dest types.UnlockHash) ([]types.Transaction, error) {
tpoolFee := types.NewCurrency64(10).Mul(types.SiacoinPrecision)
tpoolFee := types.NewCurrency64(10).Mul(types.SiacoinPrecision) // TODO: better fee algo.
output := types.SiacoinOutput{
Value: amount,
UnlockHash: dest,
......@@ -153,7 +147,7 @@ func (w *Wallet) SendSiacoins(amount types.Currency, dest types.UnlockHash) ([]t
// SendSiafunds creates a transaction sending 'amount' to 'dest'. The transaction
// is submitted to the transaction pool and is also returned.
func (w *Wallet) SendSiafunds(amount types.Currency, dest types.UnlockHash) ([]types.Transaction, error) {
tpoolFee := types.NewCurrency64(10).Mul(types.SiacoinPrecision)
tpoolFee := types.NewCurrency64(10).Mul(types.SiacoinPrecision) // TODO: better fee algo.
output := types.SiafundOutput{
Value: amount,
UnlockHash: dest,
......
......@@ -115,3 +115,61 @@ func TestNilInputs(t *testing.T) {
t.Error(err)
}
}
// TestSendSiacoins probes the SendSiacoins method of the wallet.
func TestSendSiacoins(t *testing.T) {
// Create a wallet tester.
wt, err := createWalletTester("TestSendSiacoins")
if err != nil {
t.Fatal(err)
}
// Get the initial balance - should be 1 block. The unconfirmed balances
// should be 0.
confirmedBal, _, _ := wt.wallet.ConfirmedBalance()
unconfirmedOut, unconfirmedIn := wt.wallet.UnconfirmedBalance()
if confirmedBal.Cmp(types.CalculateCoinbase(1)) != 0 {
t.Error("unexpected confirmed balance")
}
if unconfirmedOut.Cmp(types.ZeroCurrency) != 0 {
t.Error("unconfirmed balance should be 0")
}
if unconfirmedIn.Cmp(types.ZeroCurrency) != 0 {
t.Error("unconfirmed balance should be 0")
}
// Send 5000 hastings. The wallet will automatically add a fee. Outgoing
// unconfirmed siacoins - incoming unconfirmed siacoins should equal 5000 +
// fee.
tpoolFee := types.NewCurrency64(10).Mul(types.SiacoinPrecision) // TODO: tpool fee algo needs to be written.
_, err = wt.wallet.SendSiacoins(types.NewCurrency64(5000), types.UnlockHash{})
if err != nil {
t.Fatal(err)
}
confirmedBal2, _, _ := wt.wallet.ConfirmedBalance()
unconfirmedOut2, unconfirmedIn2 := wt.wallet.UnconfirmedBalance()
if confirmedBal2.Cmp(confirmedBal) != 0 {
t.Error("confirmed balance changed without introduction of blocks")
}
if unconfirmedOut2.Cmp(unconfirmedIn2.Add(types.NewCurrency64(5000)).Add(tpoolFee)) != 0 {
t.Error("sending siacoins appears to be ineffective")
}
// Move the balance into the confirmed set.
b, _ := wt.miner.FindBlock()
err = wt.cs.AcceptBlock(b)
if err != nil {
t.Fatal(err)
}
confirmedBal3, _, _ := wt.wallet.ConfirmedBalance()
unconfirmedOut3, unconfirmedIn3 := wt.wallet.UnconfirmedBalance()
if confirmedBal3.Cmp(confirmedBal2.Add(types.CalculateCoinbase(2)).Sub(types.NewCurrency64(5000)).Sub(tpoolFee)) != 0 {
t.Error("confirmed balance did not adjust to the expected value")
}
if unconfirmedOut3.Cmp(types.ZeroCurrency) != 0 {
t.Error("unconfirmed balance should be 0")
}
if unconfirmedIn3.Cmp(types.ZeroCurrency) != 0 {
t.Error("unconfirmed balance should be 0")
}
}
......@@ -35,14 +35,17 @@ var (
ErrUnlockHashWrongLen = errors.New("marshalled unlock hash is the wrong length")
ErrWholeTransactionViolation = errors.New("covered fields violation")
ZeroUnlockHash = UnlockHash{0}
// UnlockHashChecksumSize is the size of the checksum used to verify
// human-readable addresses. It is not a crypytographically secure
// checksum, it's merely intended to prevent typos. 6 is chosen because it
// brings the total size of the address to 38 bytes, leaving 2 bytes for
// potential version additions in the future.
UnlockHashChecksumSize = 6
// FullCoveredFields is a covered fileds object where the
// 'WholeTransaction' field has been set to true. The primary purpose of
// this variable is syntactic sugar.
FullCoveredFields = CoveredFields{WholeTransaction: true}
)
type (
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment