Skip to content

Commit

Permalink
add test for validateTransaction
Browse files Browse the repository at this point in the history
  • Loading branch information
fluency03 committed Apr 27, 2018
1 parent 1886627 commit 96f32ee
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 94 deletions.
3 changes: 0 additions & 3 deletions src/main/scala/com/fluency03/blockchain/Crypto.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,4 @@ object Crypto {
def privateKeyToHex(privateKey: PrivateKey): String =
privateKey.asInstanceOf[ECPrivateKey].getD.toString(16)




}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class TransactionsActor extends ActorSupport {

// TODO (Chang): need persistence
val currentTransactions: mutable.Map[String, Transaction] = mutable.Map.empty[String, Transaction]
val unspentTxOuts: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
val uTxOs: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]

val blockchainActor: ActorSelection = context.actorSelection(PARENT_UP + BLOCKCHAIN_ACTOR_NAME)
val blockActor: ActorSelection = context.actorSelection(PARENT_UP + BLOCKS_ACTOR_NAME)
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/com/fluency03/blockchain/core/Block.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ object Block {

lazy val genesisBlock: Block = genesis()

def genesis(difficulty: Int = 4): Block =
mineNextBlock(0, ZERO64, "Welcome to Blockchain in Scala!", genesisTimestamp, difficulty,
Seq(createCoinbaseTx(0, genesisMiner, genesisTimestamp)))
def genesis(difficulty: Int = 4): Block = mineNextBlock(
0, ZERO64, "Welcome to Blockchain in Scala!", genesisTimestamp, difficulty,
Seq(createCoinbaseTx(0, genesisMiner, genesisTimestamp)))

def mineNextBlock(
nextIndex: Int,
Expand Down
14 changes: 7 additions & 7 deletions src/main/scala/com/fluency03/blockchain/core/BlockHeader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ object BlockHeader {

def hashOfBlockHeader(header: BlockHeader): String =
hashOfHeaderFields(
header.index,
header.previousHash,
header.data,
header.merkleHash,
header.timestamp,
header.difficulty,
header.nonce)
header.index,
header.previousHash,
header.data,
header.merkleHash,
header.timestamp,
header.difficulty,
header.nonce)

def hashOfHeaderFields(
index: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ case class Blockchain(difficulty: Int = 4, chain: Seq[Block] = Seq(Block.genesis
def addBlock(newBlockData: String, transactions: Seq[Transaction]): Blockchain =
Blockchain(difficulty, mineNextBlock(newBlockData, transactions) +: chain)

def addBlock(newBlock: Block): Blockchain =
Blockchain(difficulty, newBlock +: chain)
def addBlock(newBlock: Block): Blockchain = Blockchain(difficulty, newBlock +: chain)

def lastBlock(): Option[Block] = chain.headOption

Expand Down
3 changes: 1 addition & 2 deletions src/main/scala/com/fluency03/blockchain/core/Merkle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ package com.fluency03.blockchain
package core

object Merkle {
def computeRoot(trans: Seq[Transaction]): String =
computeRootOfHashes(trans.map(_.id))
def computeRoot(trans: Seq[Transaction]): String = computeRootOfHashes(trans.map(_.id))

def computeRootOfHashes(hashes: Seq[String]): String = hashes.length match {
case 0 => ZERO64
Expand Down
111 changes: 63 additions & 48 deletions src/main/scala/com/fluency03/blockchain/core/Transaction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package core
import java.security.KeyPair

import com.fluency03.blockchain.Crypto.recoverPublicKey
import com.fluency03.blockchain.core.Transaction.hashOfTransaction
import com.fluency03.blockchain.core.Transaction.{hashOfTransaction, validateTransaction}
import org.json4s.native.JsonMethods.{compact, render}
import org.json4s.{Extraction, JValue}

Expand All @@ -30,6 +30,9 @@ case class Transaction(txIns: Seq[TxIn], txOuts: Seq[TxOut], timestamp: Long, id

def hasValidId: Boolean = id == hashOfTransaction(this)

def isValid(uTxOs: mutable.Map[Outpoint, TxOut]): Boolean =
hasValidId && validateTransaction(this, uTxOs)

def toJson: JValue = Extraction.decompose(this)

override def toString: String = compact(render(toJson))
Expand All @@ -40,69 +43,81 @@ object Transaction {
def apply(txIns: Seq[TxIn], txOuts: Seq[TxOut], timestamp: Long): Transaction =
Transaction(txIns, txOuts, timestamp, hashOfTransaction(txIns, txOuts, timestamp))

lazy val COINBASE_AMOUNT: Int = 50
// coinbase
final val COINBASE_AMOUNT: Int = 50

def createCoinbase(blockIndex: Int): TxIn = TxIn(Outpoint("", blockIndex), "")

def createCoinbaseTx(blockIndex: Int, miner: String, timestamp: Long): Transaction = {
val txIn = createCoinbase(blockIndex)
val txOut = TxOut(miner, COINBASE_AMOUNT)
Transaction(Seq(txIn), Seq(txOut), timestamp)
}
def createCoinbaseTx(blockIndex: Int, miner: String, timestamp: Long): Transaction =
Transaction(Seq(createCoinbase(blockIndex)), Seq(TxOut(miner, COINBASE_AMOUNT)), timestamp)

def hashOfTransaction(tx: Transaction): String =
sha256Of(tx.txIns.map(tx => tx.previousOut.id + tx.previousOut.index).mkString,
tx.txOuts.map(tx => tx.address + tx.amount).mkString, tx.timestamp.toString)
// hash of transaction
def hashOfTransaction(tx: Transaction): String = sha256Of(
tx.txIns.map(tx => tx.previousOut.id + tx.previousOut.index).mkString,
tx.txOuts.map(tx => tx.address + tx.amount).mkString,
tx.timestamp.toString)

def hashOfTransaction(txIns: Seq[TxIn], txOuts: Seq[TxOut], timestamp: Long): String =
sha256Of(txIns.map(tx => tx.previousOut.id + tx.previousOut.index).mkString,
txOuts.map(tx => tx.address + tx.amount).mkString, timestamp.toString)
def hashOfTransaction(txIns: Seq[TxIn], txOuts: Seq[TxOut], timestamp: Long): String = sha256Of(
txIns.map(tx => tx.previousOut.id + tx.previousOut.index).mkString,
txOuts.map(tx => tx.address + tx.amount).mkString,
timestamp.toString)

def signTxIn(txId: String, txIn: TxIn, keyPair: KeyPair, unspentTxOuts: mutable.Map[Outpoint, TxOut]): Option[TxIn] =
signTxIn(txId.hex2Bytes, txIn, keyPair, unspentTxOuts)
// sign TxIn
def signTxIn(txId: String, txIn: TxIn, keyPair: KeyPair, uTxOs: mutable.Map[Outpoint, TxOut]): Option[TxIn] =
signTxIn(txId.hex2Bytes, txIn, keyPair, uTxOs)

def signTxIn(txId: Bytes, txIn: TxIn, keyPair: KeyPair, unspentTxOuts: mutable.Map[Outpoint, TxOut]): Option[TxIn] =
unspentTxOuts.get(txIn.previousOut) match {
case Some(uTxO) =>
if (keyPair.getPublic.toHex != uTxO.address) None
else Some(TxIn(txIn.previousOut, Crypto.sign(txId, keyPair.getPrivate.getEncoded).toHex))
def signTxIn(txId: Bytes, txIn: TxIn, keyPair: KeyPair, uTxOs: mutable.Map[Outpoint, TxOut]): Option[TxIn] =
uTxOs.get(txIn.previousOut) match {
case Some(uTxO) => signTxIn(txId, txIn, keyPair, uTxO)
case None => None
}

def validateTxIn(txIn: TxIn, txId: String, unspentTxOuts: mutable.Map[Outpoint, TxOut]): Boolean =
validateTxIn(txIn, txId.hex2Bytes, unspentTxOuts)
def signTxIn(txId: Bytes, txIn: TxIn, keyPair: KeyPair, uTxO: TxOut): Option[TxIn] =
if (keyPair.getPublic.toHex != uTxO.address) None
else Some(TxIn(txIn.previousOut, Crypto.sign(txId, keyPair.getPrivate.getEncoded).toHex))

// validate TxIn's signature
def validateTxIn(txIn: TxIn, txId: String, uTxOs: mutable.Map[Outpoint, TxOut]): Boolean =
validateTxIn(txIn, txId.hex2Bytes, uTxOs)

def validateTxIn(txIn: TxIn, txId: Bytes, unspentTxOuts: mutable.Map[Outpoint, TxOut]): Boolean =
unspentTxOuts.get(txIn.previousOut) match {
case Some(txOut) => Crypto.verify(txId, recoverPublicKey(txOut.address).getEncoded, txIn.signature.hex2Bytes)
def validateTxIn(txIn: TxIn, txId: Bytes, uTxOs: mutable.Map[Outpoint, TxOut]): Boolean =
uTxOs.get(txIn.previousOut) match {
case Some(txOut) => validateTxIn(txId, txOut, txIn)
case None => false
}

def validateTxOutValues(transaction: Transaction, unspentTxOuts: mutable.Map[Outpoint, TxOut]): Boolean =
validateTxOutValues(transaction.txIns, transaction.txOuts, unspentTxOuts)

def validateTxOutValues(txIns: Seq[TxIn], txOuts: Seq[TxOut], unspentTxOuts: mutable.Map[Outpoint, TxOut]): Boolean = {
val totalTxInValues: Long = txIns
.map(txIn => unspentTxOuts.get(txIn.previousOut) match {
case Some(txOut) => txOut.amount
case None => 0
}).sum

val totalTxOutValues: Long = txOuts.map( _.amount).sum

totalTxInValues == totalTxOutValues
}

def validateTransaction(transaction: Transaction, unspentTxOuts: mutable.Map[Outpoint, TxOut]): Boolean =
transaction.txIns.forall(txIn => validateTxIn(txIn, transaction.id, unspentTxOuts)) &&
validateTxOutValues(transaction, unspentTxOuts)

def updateUTxOs(transactions: Seq[Transaction], unspentTxOuts: Map[Outpoint, TxOut]): Map[Outpoint, TxOut] = {
val newUnspentTxOuts = getNewUTxOs(transactions)
def validateTxIn(txId: Bytes, txOut: TxOut, txIn: TxIn): Boolean =
Crypto.verify(txId, recoverPublicKey(txOut.address).getEncoded, txIn.signature.hex2Bytes)

// validate TxOut: Sum of TxOuts is equal to the sum of TxIns
def validateTxOutValues(transaction: Transaction, uTxOs: mutable.Map[Outpoint, TxOut]): Boolean =
validateTxOutValues(transaction.txIns, transaction.txOuts, uTxOs)

def validateTxOutValues(txIns: Seq[TxIn], txOuts: Seq[TxOut], uTxOs: mutable.Map[Outpoint, TxOut]): Boolean =
txIns.map(txIn => uTxOs.get(txIn.previousOut) match {
case Some(txOut) => txOut.amount
case None => 0
}).sum == txOuts.map( _.amount).sum

/**
* Validate Transaction:
* 1. All TxIns are valid, i.e., has valid signature
* 2. Sum of TxOuts is equal to the sum of TxIns
*/
def validateTransaction(transaction: Transaction, uTxOs: mutable.Map[Outpoint, TxOut]): Boolean =
transaction.txIns.forall(txIn => validateTxIn(txIn, transaction.id, uTxOs)) &&
validateTxOutValues(transaction, uTxOs)

/**
* Update UTXOs:
* 1. Remove all consumed unspent transaction outputs
* 2. Append all new unspent transaction outputs
*/
def updateUTxOs(transactions: Seq[Transaction], uTxOs: Map[Outpoint, TxOut]): Map[Outpoint, TxOut] = {
val consumedTxOuts = getConsumedUTxOs(transactions)
unspentTxOuts.filterNot {
uTxOs.filterNot {
case (i, _) => consumedTxOuts.contains(i)
} ++ newUnspentTxOuts
} ++ getNewUTxOs(transactions)
}

def getNewUTxOs(transactions: Seq[Transaction]): Map[Outpoint, TxOut] =
Expand Down
84 changes: 56 additions & 28 deletions src/test/scala/com/fluency03/blockchain/core/TransactionTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ class TransactionTest extends FlatSpec with Matchers {
val signature = Crypto.sign(hash.hex2Bytes, pair.getPrivate.getEncoded)
Crypto.verify(hash.hex2Bytes, pair.getPublic.getEncoded, signature) shouldEqual true

val unspentTxOuts: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
val signedTxIn0 = signTxIn(hash, txIn, pair, unspentTxOuts)
val uTxOs: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
val signedTxIn0 = signTxIn(hash, txIn, pair, uTxOs)
signedTxIn0 shouldEqual None

unspentTxOuts += (Outpoint("def0", 0) -> TxOut(pair.getPublic.toHex, 40))
unspentTxOuts += (Outpoint("def0", 1) -> TxOut("abc4", 40))
uTxOs += (Outpoint("def0", 0) -> TxOut(pair.getPublic.toHex, 40))
uTxOs += (Outpoint("def0", 1) -> TxOut("abc4", 40))

val signedTxIn = signTxIn(hash, txIn, pair, unspentTxOuts)
val signedTxIn = signTxIn(hash, txIn, pair, uTxOs)
signedTxIn shouldEqual Some(TxIn(Outpoint("def0", 0), signedTxIn.get.signature))

signedTxIn.get.previousOut shouldEqual Outpoint("def0", 0)
Expand All @@ -120,37 +120,36 @@ class TransactionTest extends FlatSpec with Matchers {
Crypto.verify(hash.hex2Bytes, pair.getPublic.getEncoded, signature) shouldEqual true

val txIn = TxIn(Outpoint("def0", 0), "abc1")
val unspentTxOuts: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
unspentTxOuts += (Outpoint("def0", 0) -> TxOut(pair.getPublic.toHex, 40))
unspentTxOuts += (Outpoint("def0", 1) -> TxOut("abc4", 40))
val uTxOs: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
uTxOs += (Outpoint("def0", 0) -> TxOut(pair.getPublic.toHex, 40))
uTxOs += (Outpoint("def0", 1) -> TxOut("abc4", 40))

val signedTxIn = signTxIn(hash, txIn, pair, unspentTxOuts)
val signedTxIn = signTxIn(hash, txIn, pair, uTxOs)

val unspentTxOuts0: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
val uTxOs0: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]

validateTxIn(signedTxIn.get, hash, unspentTxOuts0) shouldEqual false
validateTxIn(signedTxIn.get, hash, unspentTxOuts) shouldEqual true
validateTxIn(signedTxIn.get, hash, uTxOs0) shouldEqual false
validateTxIn(signedTxIn.get, hash, uTxOs) shouldEqual true
}

"Transaction" should "have valid TxOut values." in {
val pair: KeyPair = Crypto.generateKeyPair()

val unspentTxOuts: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
unspentTxOuts += (Outpoint("def0", 1) -> TxOut("abc4", 20))
val uTxOs: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
uTxOs += (Outpoint("def0", 1) -> TxOut("abc4", 20))

val tx: Transaction = Transaction(
Seq(TxIn(Outpoint("def0", 0), "abc1"), TxIn(Outpoint("def0", 1), "abc1")),
Seq(TxOut("abc4", 40)),
genesisTimestamp
)

validateTxOutValues(tx, unspentTxOuts) shouldEqual false
validateTxOutValues(tx, uTxOs) shouldEqual false

unspentTxOuts += (Outpoint("def0", 0) -> TxOut("abc1", 20))
validateTxOutValues(tx, unspentTxOuts) shouldEqual true
uTxOs += (Outpoint("def0", 0) -> TxOut("abc1", 20))
validateTxOutValues(tx, uTxOs) shouldEqual true
}


"updateUTxOs" should "update the UTXOs from a latest Seq of transactions." in {
val tx: Transaction = Transaction(
Seq(TxIn(Outpoint("def0", 0), "abc1"),
Expand All @@ -159,22 +158,51 @@ class TransactionTest extends FlatSpec with Matchers {
genesisTimestamp
)

val unspentTxOuts1: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
unspentTxOuts1 += (Outpoint("def0", 0) -> TxOut("abc4", 20))
unspentTxOuts1 += (Outpoint("def0", 1) -> TxOut("abc4", 20))
val uTxOs1: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
uTxOs1 += (Outpoint("def0", 0) -> TxOut("abc4", 20))
uTxOs1 += (Outpoint("def0", 1) -> TxOut("abc4", 20))

val unspentTxOuts2: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
updateUTxOs(Seq(tx), unspentTxOuts1.toMap) should not equal unspentTxOuts2
val uTxOs2: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
updateUTxOs(Seq(tx), uTxOs1.toMap) should not equal uTxOs2

unspentTxOuts2 += (Outpoint(tx.id, 0) -> TxOut("abc4", 40))
updateUTxOs(Seq(tx), unspentTxOuts1.toMap) shouldEqual unspentTxOuts2
uTxOs2 += (Outpoint(tx.id, 0) -> TxOut("abc4", 40))
updateUTxOs(Seq(tx), uTxOs1.toMap) shouldEqual uTxOs2
}


"Transaction" should "have be validatable." in {
// TODO (Chang): validateTransaction
}
val pair1 = Crypto.generateKeyPair()
val address1 = pair1.getPublic.toHex
val pair2 = Crypto.generateKeyPair()
val address2 = pair2.getPublic.toHex
val randHash = "".toSha256
val tx: Transaction = Transaction(
Seq(TxIn(Outpoint(randHash, 0), ""),
TxIn(Outpoint(randHash, 1), "")),
Seq(TxOut(address2, 40)),
genesisTimestamp
)

val uTxOs: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
val signedTxIns = tx.txIns.map(txIn => signTxIn(tx.id.hex2Bytes, txIn, pair1, uTxOs)).filter(_.isDefined).map(_.get)
signedTxIns.length should not equal tx.txIns.length
val signedTx = Transaction(
signedTxIns,
Seq(TxOut(address2, 40)),
genesisTimestamp)

signedTx.isValid(uTxOs) shouldEqual false

uTxOs += (Outpoint(randHash, 0) -> TxOut(address1, 20))
uTxOs += (Outpoint(randHash, 1) -> TxOut(address1, 20))

val signedTxIns2 = tx.txIns.map(txIn => signTxIn(tx.id.hex2Bytes, txIn, pair1, uTxOs)).filter(_.isDefined).map(_.get)
signedTxIns2.length shouldEqual tx.txIns.length
val signedTx2 = Transaction(
signedTxIns2,
Seq(TxOut(address2, 40)),
genesisTimestamp)

signedTx2.isValid(uTxOs) shouldEqual true
}

}

0 comments on commit 96f32ee

Please sign in to comment.