diff --git a/Makefile b/Makefile index 9c5ce5b..fee43d1 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ run-console: cumulus deps: glide install -clean: +clean: cumulus rm cumulus install-glide: diff --git a/app/app.go b/app/app.go index 7a62299..f791717 100644 --- a/app/app.go +++ b/app/app.go @@ -251,11 +251,17 @@ func (a *App) HandleBlock(blk *blockchain.Block) { // the next block so that the block // numbers make sense. a.Chain.AppendBlock(blk) - address := a.CurrentUser.Wallet.Public() - blk := a.Pool.NextBlock(a.Chain, address, a.CurrentUser.BlockSize) + + // Drop pending transactions (if they occur in this block). + a.CurrentUser.Wallet.DropAllPending(blk.Transactions) + + // Handle miner behaviour (set up a new block). if miner.IsMining() { + address := a.CurrentUser.Wallet.Public() + blk := a.Pool.NextBlock(a.Chain, address, a.CurrentUser.BlockSize) miner.RestartMiner(a.Chain, blk) } + log.Debug("added blk number %d to chain", blk.BlockNumber) } } diff --git a/app/app_test.go b/app/app_test.go index 40dfef6..4ead25d 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -178,3 +178,18 @@ func TestHandleTransaction(t *testing.T) { transactionQueue <- blockchain.NewTestTransaction() assert.Equal(t, len(transactionQueue), 0) } + +func TestPay(t *testing.T) { + amt := uint64(5) + a := createNewTestApp() + err := a.Pay("badf00d", amt) + + // Fail with low balance. + assert.NotNil(t, err) + + a.CurrentUser.Wallet.SetBalance(amt) + err = a.Pay("badf00d", amt) + + // Fail with bad inputs. + assert.NotNil(t, err) +} diff --git a/app/console.go b/app/console.go index 5865e61..a6267e1 100644 --- a/app/console.go +++ b/app/console.go @@ -65,22 +65,9 @@ func create(ctx *ishell.Context, app *App) { "Transaction", }, "What would you like to create?") if choice == 0 { - createHotWallet(ctx, app) + createWallet(ctx, app) } else { - shell.Print("Sender wallet ID: ") - senderID := shell.ReadLine() - shell.Print("Recipient wallet ID: ") - recipientID := shell.ReadLine() - shell.Print("Amount to send: ") - amount, err := strconv.ParseFloat(shell.ReadLine(), 64) - if err != nil { - shell.Println("Invalid number format. Please enter an amount in decimal format.") - return - } - - // TODO: make transaction, add it to the pool, broadcast it - ctx.Printf(`\nNew Transaction: \nSenderID: %s \nRecipiendID: %s\nAmount: %f"`, - senderID, recipientID, amount) + createTransaction(ctx, app) } } @@ -119,13 +106,40 @@ func connect(ctx *ishell.Context, a *App) { } } -func createHotWallet(ctx *ishell.Context, app *App) { - shell.Print("Enter wallet name: ") - walletName := shell.ReadLine() - wallet := HotWallet{walletName, blockchain.NewWallet()} - app.CurrentUser.HotWallet = wallet - emoji.Println(":credit_card: New hot wallet created!") - emoji.Println(":raising_hand: Name: " + wallet.Name) - emoji.Println(":mailbox: Address: " + wallet.Wallet.Public().Repr()) - emoji.Println(":fist: Emoji Address: " + wallet.Wallet.Public().Emoji()) +func createWallet(ctx *ishell.Context, app *App) { + // Create a new wallet and set as CurrentUser's wallet. + wallet := blockchain.NewWallet() + app.CurrentUser.Wallet = wallet + emoji.Println(":credit_card: New wallet created!") + + // Give a printout of the address(es). + emoji.Print(":mailbox:") + ctx.Println(" Address: " + wallet.Public().Repr()) + emoji.Println(":fist: Emoji Address: " + wallet.Public().Emoji()) + ctx.Println("") +} + +func createTransaction(ctx *ishell.Context, app *App) { + // Read in the recipient address. + emoji.Print(":credit_card:") + ctx.Println(" Enter recipient wallet address") + toAddress := shell.ReadLine() + + // Get amount from user. + emoji.Print(":dollar:") + ctx.Println(" Enter amount to send: ") + amount, err := strconv.ParseUint(shell.ReadLine(), 10, 64) + if err != nil { + emoji.Println(":disappointed: Invalid number format. Please enter an amount in decimal format.") + return + } + + // Try to make a payment. + err = app.Pay(toAddress, amount) + if err != nil { + emoji.Println(":disappointed: Transaction failed!") + ctx.Println(err.Error) + } else { + emoji.Println(":mailbox_with_mail: Its in the mail!") + } } diff --git a/app/user.go b/app/user.go index 8f9a490..af1bf70 100644 --- a/app/user.go +++ b/app/user.go @@ -1,33 +1,76 @@ package app -import "github.com/ubclaunchpad/cumulus/blockchain" +import ( + "errors" + + "github.com/ubclaunchpad/cumulus/blockchain" + "github.com/ubclaunchpad/cumulus/msg" + + crand "crypto/rand" +) // User holds basic user information. type User struct { - HotWallet + *blockchain.Wallet BlockSize uint32 } -// HotWallet is a representation of the users wallet. -type HotWallet struct { - Name string - blockchain.Wallet -} - // NewUser creates a new user func NewUser() *User { return &User{ - HotWallet: HotWallet{ - Wallet: blockchain.NewWallet(), - Name: "default", - }, + Wallet: blockchain.NewWallet(), BlockSize: blockchain.DefaultBlockSize, } } -// getCurrentUser gets the current user. +// getCurrentUser gets the current user function only used for app initalization. func getCurrentUser() *User { // TODO: Check for local user information on disk, // If doesnt exist, create new user. return NewUser() } + +// Pay pays an amount of coin to an address `to`. +func (a *App) Pay(to string, amount uint64) error { + // Four atomic steps must occur. + wallet := a.CurrentUser.Wallet + pool := a.Pool + + // A legitimate transaction must be built. + tbody := blockchain.TxBody{ + Sender: wallet.Public(), + // TODO: Collect inputs. + Input: blockchain.TxHashPointer{}, + Outputs: []blockchain.TxOutput{ + blockchain.TxOutput{ + Recipient: to, + Amount: amount, + }, + }, + } + + // The transaction must be signed. + if txn, err := tbody.Sign(*a.CurrentUser.Wallet, crand.Reader); err == nil { + + // The transaction must be broadcasted to the peers. + if err := wallet.SetPending(txn); err != nil { + return err + } + + // The transaction must be added to the pool. + if !pool.Push(txn, a.Chain) { + return errors.New("transaction broadcasted, but could not be added to the pool") + } + + // The transaction must be broadcasted to the network. + a.PeerStore.Broadcast(msg.Push{ + ResourceType: msg.ResourceTransaction, + Resource: txn, + }) + + } else { + return err + } + + return nil +} diff --git a/blockchain/genesis.go b/blockchain/genesis.go index 0e45413..2cc99a8 100644 --- a/blockchain/genesis.go +++ b/blockchain/genesis.go @@ -13,7 +13,7 @@ func Genesis(miner Address, target Hash, blockReward uint64, extraData []byte) * cbReward := TxOutput{ Amount: blockReward, - Recipient: miner, + Recipient: miner.Repr(), } cbTx := &Transaction{ diff --git a/blockchain/test_utils.go b/blockchain/test_utils.go index 34d7c49..738625c 100644 --- a/blockchain/test_utils.go +++ b/blockchain/test_utils.go @@ -30,7 +30,7 @@ func NewTestTxHashPointer() TxHashPointer { func NewTestTxOutput() TxOutput { return TxOutput{ Amount: uint64(mrand.Int63()), - Recipient: NewWallet().Public(), + Recipient: NewWallet().Public().Repr(), } } @@ -53,7 +53,7 @@ func NewTestTxBody() TxBody { func NewTestTransaction() *Transaction { sender := NewWallet() tbody := NewTestTxBody() - t, _ := tbody.Sign(sender, crand.Reader) + t, _ := tbody.Sign(*sender, crand.Reader) return t } @@ -124,7 +124,7 @@ func NewTestOutputBlock(t []*Transaction, input *Block) *Block { } // NewTestTransactionValue creates a new transaction with specific value a. -func NewTestTransactionValue(s, r Wallet, a uint64, i uint32) (*Transaction, error) { +func NewTestTransactionValue(s, r *Wallet, a uint64, i uint32) (*Transaction, error) { tbody := TxBody{ Sender: s.Public(), Input: TxHashPointer{ @@ -136,9 +136,9 @@ func NewTestTransactionValue(s, r Wallet, a uint64, i uint32) (*Transaction, err } tbody.Outputs[0] = TxOutput{ Amount: a, - Recipient: r.Public(), + Recipient: r.Public().Repr(), } - return tbody.Sign(s, crand.Reader) + return tbody.Sign(*s, crand.Reader) } // NewValidBlockChainFixture creates a valid blockchain of two blocks. @@ -150,13 +150,13 @@ func NewValidBlockChainFixture() (*BlockChain, Wallet) { trA, _ := NewTestTransactionValue(original, sender, 2, 1) trA.Outputs = append(trA.Outputs, TxOutput{ Amount: 2, - Recipient: sender.Public(), + Recipient: sender.Public().Repr(), }) trB, _ := NewTestTransactionValue(sender, recipient, 4, 1) trB.Input.Hash = HashSum(trA) - trB, _ = trB.TxBody.Sign(sender, crand.Reader) + trB, _ = trB.TxBody.Sign(*sender, crand.Reader) cbA, _ := NewValidCloudBaseTestTransaction() cbB, _ := NewValidCloudBaseTestTransaction() @@ -169,7 +169,7 @@ func NewValidBlockChainFixture() (*BlockChain, Wallet) { return &BlockChain{ Blocks: []*Block{inputBlock, outputBlock}, Head: NewTestHash(), - }, recipient + }, *recipient } // NewValidTestChainAndBlock creates a valid BlockChain and a Block that is valid @@ -192,7 +192,7 @@ func NewValidTestChainAndBlock() (*BlockChain, *Block) { } tbody.Outputs[0] = TxOutput{ Amount: a, - Recipient: NewWallet().Public(), + Recipient: NewWallet().Public().Repr(), } tr, _ := tbody.Sign(s, crand.Reader) @@ -223,7 +223,7 @@ func NewValidCloudBaseTestTransaction() (*Transaction, Address) { } cbReward := TxOutput{ Amount: 25, - Recipient: w.Public(), + Recipient: w.Public().Repr(), } cbTxBody := TxBody{ Sender: NilAddr, diff --git a/blockchain/transaction.go b/blockchain/transaction.go index 6b5e464..c7e963f 100644 --- a/blockchain/transaction.go +++ b/blockchain/transaction.go @@ -26,14 +26,14 @@ func (thp TxHashPointer) Marshal() []byte { // TxOutput defines an output to a transaction type TxOutput struct { Amount uint64 - Recipient Address + Recipient string } // Marshal converts a TxOutput to a byte slice func (to TxOutput) Marshal() []byte { buf := make([]byte, 8) binary.LittleEndian.PutUint64(buf, to.Amount) - buf = append(buf, to.Recipient.Marshal()...) + buf = append(buf, []byte(to.Recipient)...) return buf } @@ -103,3 +103,12 @@ func (t *Transaction) InputsEqualOutputs(other ...*Transaction) bool { return (int(outAmount) - int(inAmount)) == 0 } + +// GetTotalOutput sums the output amounts from the transaction. +func (t *Transaction) GetTotalOutput() uint64 { + result := uint64(0) + for _, out := range t.Outputs { + result += out.Amount + } + return result +} diff --git a/blockchain/transaction_test.go b/blockchain/transaction_test.go index 72db01b..b9d1d53 100644 --- a/blockchain/transaction_test.go +++ b/blockchain/transaction_test.go @@ -2,29 +2,38 @@ package blockchain import ( "testing" + + "github.com/stretchr/testify/assert" ) func TestTxBodyLen(t *testing.T) { txBody := NewTestTxBody() senderLen := AddrLen inputLen := 2*(32/8) + HashLen - outputLen := len(txBody.Outputs) * (64/8 + AddrLen) + outputLen := len(txBody.Outputs) * (64/8 + ReprLen) txBodyLen := senderLen + inputLen + outputLen - if txBody.Len() != txBodyLen { - t.Fail() - } + assert.Equal(t, txBody.Len(), txBodyLen) } func TestTransactionLen(t *testing.T) { tx := NewTestTransaction() senderLen := AddrLen inputLen := 2*(32/8) + HashLen - outputLen := len(tx.TxBody.Outputs) * (64/8 + AddrLen) + outputLen := len(tx.TxBody.Outputs) * (64/8 + ReprLen) txBodyLen := senderLen + inputLen + outputLen txLen := txBodyLen + SigLen - if tx.Len() != txLen { - t.Fail() + assert.Equal(t, tx.Len(), txLen) +} + +func TestTransactionGetTotalOutput(t *testing.T) { + tx := NewTestTransaction() + tx.Outputs = []TxOutput{ + TxOutput{ + Recipient: tx.Outputs[0].Recipient, + Amount: 5, + }, } + assert.Equal(t, tx.GetTotalOutput(), uint64(5)) } diff --git a/blockchain/wallet.go b/blockchain/wallet.go index 2a0dfae..0fc4e16 100644 --- a/blockchain/wallet.go +++ b/blockchain/wallet.go @@ -6,9 +6,12 @@ import ( crand "crypto/rand" "crypto/sha256" "encoding/hex" + "errors" + "fmt" "io" "math/big" + log "github.com/Sirupsen/logrus" c "github.com/ubclaunchpad/cumulus/common/constants" "github.com/ubclaunchpad/cumulus/moj" ) @@ -18,6 +21,8 @@ const ( CoordLen = 32 // AddrLen is the length in bytes of addresses. AddrLen = 2 * CoordLen + // ReprLen is the length in bytes of an address checksum. + ReprLen = 40 // SigLen is the length in bytes of signatures. SigLen = AddrLen // AddressVersion is the version of the address shortening protocol. @@ -93,27 +98,31 @@ func (a Address) Key() *ecdsa.PublicKey { } } -// Wallet represents a wallet that we have the ability to sign for. -type Wallet interface { +// Account represents a wallet that we have the ability to sign for. +type Account interface { Public() Address Sign(digest Hash, random io.Reader) (Signature, error) } -// Internal representation of a wallet. -type wallet ecdsa.PrivateKey +// Wallet is an account that can sign and hold a balance. +type Wallet struct { + *ecdsa.PrivateKey + PendingTxns []*Transaction + Balance uint64 +} // Key retreives the underlying private key from a wallet. -func (w *wallet) key() *ecdsa.PrivateKey { - return (*ecdsa.PrivateKey)(w) +func (w *Wallet) key() *ecdsa.PrivateKey { + return w.PrivateKey } // Public returns the public key as byte array, or address, of the wallet. -func (w *wallet) Public() Address { - return Address{X: w.PublicKey.X, Y: w.PublicKey.Y} +func (w *Wallet) Public() Address { + return Address{X: w.PrivateKey.PublicKey.X, Y: w.PrivateKey.PublicKey.Y} } // Sign returns a signature of the digest. -func (w *wallet) Sign(digest Hash, random io.Reader) (Signature, error) { +func (w *Wallet) Sign(digest Hash, random io.Reader) (Signature, error) { r, s, err := ecdsa.Sign(random, w.key(), digest.Marshal()) return Signature{R: r, S: s}, err } @@ -147,9 +156,82 @@ func (s *Signature) Marshal() []byte { return buf } -// NewWallet produces a new Wallet that can sign transactionsand has a +// NewWallet produces a new Wallet that can sign transactions and has a // public Address. -func NewWallet() Wallet { +func NewWallet() *Wallet { priv, _ := ecdsa.GenerateKey(curve, crand.Reader) - return (*wallet)(priv) + return &Wallet{ + PrivateKey: priv, + Balance: 0, + } +} + +// SetAllPending appends transactions to the pending set of transactions. +func (w *Wallet) SetAllPending(txns []*Transaction) { + for _, t := range txns { + w.SetPending(t) + } +} + +// SetPending appends one transaction to the pending set of transaction +// if the wallet effective balance is high enough to accomodate. +func (w *Wallet) SetPending(txn *Transaction) error { + bal := w.GetEffectiveBalance() + spend := txn.GetTotalOutput() + if bal >= spend { + w.PendingTxns = append(w.PendingTxns, txn) + } else { + msg := fmt.Sprintf("wallet balance is too low %v < %v", bal, spend) + return errors.New(msg) + } + return nil +} + +// DropAllPending drops pending transactions if they apper in txns. +func (w *Wallet) DropAllPending(txns []*Transaction) { + for _, t := range txns { + if p, i := w.IsPending(t); p { + w.DropPending(i) + } + } +} + +// DropPending a single pending transaction by index in the pending list. +func (w *Wallet) DropPending(i int) { + if i < len(w.PendingTxns) && i >= 0 { + log.Info("dropping transaction with hash %s", w.PendingTxns[i].Input.Hash) + w.PendingTxns = append(w.PendingTxns[:i], w.PendingTxns[i+1:]...) + } +} + +// IsPending returns true if the transaction exists in the pending list. +// If true, it also returns the integer index of the transaction. +func (w *Wallet) IsPending(txn *Transaction) (bool, int) { + for i, t := range w.PendingTxns { + if t.Input.Hash == txn.Input.Hash { + return true, i + } + } + return false, -1 +} + +// GetEffectiveBalance returns the wallet balance less the sum of the pending +// transactions in the wallet. +func (w *Wallet) GetEffectiveBalance() uint64 { + r := w.Balance + for _, t := range w.PendingTxns { + r -= t.Outputs[0].Amount + } + return r +} + +// GetBalance returns the raw balance without calculating pending transactions. +func (w *Wallet) GetBalance() uint64 { + // TODO: Get historical wallet activity, cache and update block by block. + return w.Balance +} + +// SetBalance idempotently sets the account balance. +func (w *Wallet) SetBalance(b uint64) { + w.Balance = b } diff --git a/blockchain/wallet_test.go b/blockchain/wallet_test.go new file mode 100644 index 0000000..38f5b4c --- /dev/null +++ b/blockchain/wallet_test.go @@ -0,0 +1,43 @@ +package blockchain + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewWallet(t *testing.T) { + w := NewWallet() + assert.Equal(t, w.GetEffectiveBalance(), uint64(0)) + assert.Equal(t, len(w.PendingTxns), 0) +} + +func TestSetAllPending(t *testing.T) { + w := NewWallet() + txn := NewTestTransaction() + + // Set the balance approprately to handle the txn. + w.SetBalance(txn.GetTotalOutput()) + + // Set and check. + w.SetAllPending([]*Transaction{txn}) + result, i := w.IsPending(txn) + + // Make sure transaction is actually pending. + assert.True(t, result) + assert.Equal(t, i, 0) +} + +func TestDropAllPending(t *testing.T) { + w := NewWallet() + txn := NewTestTransaction() + w.SetBalance(txn.GetTotalOutput()) + w.SetAllPending([]*Transaction{txn}) + + // Drop all pending + result, _ := w.IsPending(txn) + assert.True(t, result) + w.DropAllPending([]*Transaction{txn}) + result, _ = w.IsPending(txn) + assert.False(t, result) +} diff --git a/consensus/consensus.go b/consensus/consensus.go index d77415d..70fec24 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -82,7 +82,7 @@ func VerifyCloudBase(bc *blockchain.BlockChain, reward := StartingBlockReward / uint64(math.Pow(float64(2), timesHalved)) // Check that the output is properly set. - if len(t.Outputs) != 1 || t.Outputs[0].Recipient == blockchain.NilAddr { + if len(t.Outputs) != 1 || t.Outputs[0].Recipient == blockchain.NilAddr.Repr() { return false, BadCloudBaseOutput } diff --git a/consensus/consensus_test.go b/consensus/consensus_test.go index 3fef4fa..dd6de63 100644 --- a/consensus/consensus_test.go +++ b/consensus/consensus_test.go @@ -68,7 +68,7 @@ func TestVerifyTransactionSignatureFail(t *testing.T) { tr := bc.Blocks[1].Transactions[1] fakeSender := blockchain.NewWallet() - tr, _ = tr.TxBody.Sign(fakeSender, crand.Reader) + tr, _ = tr.TxBody.Sign(*fakeSender, crand.Reader) bc.Blocks[1].Transactions[1] = tr valid, code := VerifyTransaction(bc, tr) @@ -405,7 +405,7 @@ func TestVerifyCloudBaseBadOutput(t *testing.T) { b.GetCloudBaseTransaction().Outputs, blockchain.TxOutput{ Amount: 25, - Recipient: w.Public(), + Recipient: w.Public().Repr(), }, ) valid, code := VerifyCloudBase(bc, b.GetCloudBaseTransaction()) @@ -432,7 +432,7 @@ func TestVerifyCloudBaseBadOutput(t *testing.T) { bc, _ = blockchain.NewValidBlockChainFixture() b = bc.Blocks[0] - b.GetCloudBaseTransaction().Outputs[0].Recipient = blockchain.NilAddr + b.GetCloudBaseTransaction().Outputs[0].Recipient = blockchain.NilAddr.Repr() valid, code = VerifyCloudBase(bc, b.GetCloudBaseTransaction()) if valid { diff --git a/miner/miner.go b/miner/miner.go index a32cbc8..7d94401 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -104,7 +104,7 @@ func CloudBase( // TODO: Add transaction fees cbReward := blockchain.TxOutput{ Amount: consensus.CurrentBlockReward(bc), - Recipient: cb, + Recipient: cb.Repr(), } cbTxBody := blockchain.TxBody{ Sender: blockchain.NilAddr, diff --git a/miner/miner_test.go b/miner/miner_test.go index 2ec6a30..a9ec247 100644 --- a/miner/miner_test.go +++ b/miner/miner_test.go @@ -75,7 +75,7 @@ func TestCloudBase(t *testing.T) { t.Fail() } - if b.Transactions[0].Outputs[0].Recipient != w.Public() { + if b.Transactions[0].Outputs[0].Recipient != w.Public().Repr() { t.Fail() } } diff --git a/pool/pool.go b/pool/pool.go index 5381361..0a53726 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -71,6 +71,7 @@ func getIndex(a []*PooledTransaction, target time.Time, low, high int) int { // Push inserts a transaction into the pool, returning // true if the Transaction was inserted (was valid). +// TODO: This should return an error if could not add. func (p *Pool) Push(t *blockchain.Transaction, bc *blockchain.BlockChain) bool { if ok, err := consensus.VerifyTransaction(bc, t); ok { p.set(t)