Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Transaction Command #120

Merged
merged 10 commits into from
Aug 13, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ run-console: cumulus
deps:
glide install

clean:
clean: cumulus
rm cumulus

install-glide:
Expand Down
58 changes: 34 additions & 24 deletions app/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -119,13 +106,36 @@ 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) {
wallet := blockchain.NewWallet()
app.CurrentUser.Wallet = wallet
emoji.Println(":credit_card: New wallet created!")
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) {
emoji.Print(":credit_card:")
ctx.Println(" Enter recipient wallet address")
toAddress := shell.ReadLine()
// TODO: Error handling address input.

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
}

// TODO: Check if we have enough coins to make the purchase.
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!")
}
}
54 changes: 41 additions & 13 deletions app/user.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,61 @@
package app

import "github.com/ubclaunchpad/cumulus/blockchain"
import (
"github.com/ubclaunchpad/cumulus/blockchain"

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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to make some changes to this function.

We should:

  1. Create a signed transaction
  2. Broadcast it to all connected nodes

Independently of this function, the balance of the current user's wallet should be computed each time a new block is added to our chain.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, so the wallet balance is only updated when our transaction has landed in a block - makes total sense.

// Three atomic steps must occur.
// 1. The amount must be debited from the wallet.
err := a.CurrentUser.Wallet.Debit(amount)
if err == nil {
return err
}

// 2. A legitimate transaction must be built.
tbody := blockchain.TxBody{
Sender: getCurrentUser().Wallet.Public(),
Outputs: []blockchain.TxOutput{
blockchain.TxOutput{
Recipient: to,
Amount: amount,
},
},
}

// 3. The transaction must be signed and added to the pool.
if txn, err := tbody.Sign(*a.CurrentUser.Wallet, crand.Reader); err != nil {
a.HandleTransaction(txn)
} else {
// Signature failed, credit the account, return an error.
// Crediting cannot fail because debiting succeeded and process is single threaded.
a.CurrentUser.Wallet.Credit(amount)
return err
}

return nil
}
2 changes: 1 addition & 1 deletion blockchain/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
20 changes: 10 additions & 10 deletions blockchain/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func NewTestTxHashPointer() TxHashPointer {
func NewTestTxOutput() TxOutput {
return TxOutput{
Amount: uint64(mrand.Int63()),
Recipient: NewWallet().Public(),
Recipient: NewWallet().Public().Repr(),
}
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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{
Expand All @@ -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.
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -223,7 +223,7 @@ func NewValidCloudBaseTestTransaction() (*Transaction, Address) {
}
cbReward := TxOutput{
Amount: 25,
Recipient: w.Public(),
Recipient: w.Public().Repr(),
}
cbTxBody := TxBody{
Sender: NilAddr,
Expand Down
4 changes: 2 additions & 2 deletions blockchain/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
14 changes: 6 additions & 8 deletions blockchain/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,27 @@ 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)
}
58 changes: 46 additions & 12 deletions blockchain/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
crand "crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"math"
"math/big"

c "github.com/ubclaunchpad/cumulus/common/constants"
Expand All @@ -18,6 +20,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.
Expand Down Expand Up @@ -93,27 +97,32 @@ 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)
Debit(amount uint64) error
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above, I don't think we should modify a user account directly, it should be a function of the blockchain and should be recomputed each time we add a block to the chain.

Credit(amount uint64) 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
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
}
Expand Down Expand Up @@ -147,9 +156,34 @@ 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,
}
}

// Debit attempts to spend some coin from the wallet.
func (w *Wallet) Debit(amount uint64) error {
if amount >= w.Balance {
return errors.New("wallet does not have a high enough balance to satisfy the request")
} else if amount < 0 {
return errors.New("debit amounts may not be negative")
}
w.Balance -= amount
return nil
}

// Credit attempts to add count to the wallet.
func (w *Wallet) Credit(amount uint64) error {
if amount >= math.MaxUint64 {
return errors.New("wallet cannot hold this amount")
} else if amount < 0 {
return errors.New("credit amounts may not be negative")
}
w.Balance += amount
return nil
}
Loading