Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
fluency03 committed Apr 27, 2018
2 parents 2115294 + eceee02 commit 4dfacec
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 10 deletions.
36 changes: 32 additions & 4 deletions src/main/scala/com/fluency03/blockchain/core/Block.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.fluency03.blockchain
package core

import com.fluency03.blockchain.core.Block.allTransValidOf
import com.fluency03.blockchain.core.BlockHeader.hashOfHeaderFields
import com.fluency03.blockchain.core.Transaction.createCoinbaseTx
import com.fluency03.blockchain.core.Transaction.{createCoinbaseTx, validateCoinbaseTx, noDuplicateTxInOf}
import org.json4s.native.JsonMethods.{compact, render}
import org.json4s.{Extraction, JValue}

import scala.collection.mutable

/**
* Block on the chain.
* @param header Header of current Block
Expand Down Expand Up @@ -37,6 +40,10 @@ case class Block(header: BlockHeader, transactions: Seq[Transaction], hash: Stri

def hasValidHeaderHash: Boolean = hash == header.hash

def allTransAreValid(uTxOs: mutable.Map[Outpoint, TxOut]): Boolean = allTransValidOf(transactions, index, uTxOs)

def noDuplicateTxIn(): Boolean = noDuplicateTxInOf(transactions)

def toJson: JValue = Extraction.decompose(this)

override def toString: String = compact(render(toJson))
Expand Down Expand Up @@ -112,10 +119,31 @@ object Block {
transactions: Seq[Transaction]): Block =
mineNextBlock(currentBlock.index + 1, currentBlock.hash, newBlockData, timestamp, difficulty, transactions)

// TODO (Chang): Check transactions in the new block?
def canBeChained(newBlock: Block, previousBlock: Block): Boolean =
previousBlock.index + 1 == newBlock.index && previousBlock.hash == newBlock.previousHash && newBlock.hasValidHash
/**
* Check whether transactions of a Block are valid:
* 1. Coinbase transaction is valid
* 2. Rest of the transactions are valid
* 3. if the Seq is empty, then it is not valid, because it has to at least contain one coinbase transaction
*/
def allTransValidOf(
transactions: Seq[Transaction],
blockIndex: Int,
uTxOs: mutable.Map[Outpoint, TxOut])
: Boolean = transactions match {
case Nil => false
case init :+ last => validateCoinbaseTx(last, blockIndex) && init.forall(tx => tx.isValid(uTxOs))
}

/**
* Check whether a new Block can be chained to the last Block of a Blockchain (previousBlock of newBlock):
* 1. New Block's index should be the previous index plus one
* 2. New Block's previousHash should be the hash of previous Block
* 3. New Block should have valid hash (that means, valid header hash and merkle hash)
*/
def validLinkBetween(newBlock: Block, previousBlock: Block): Boolean =
previousBlock.index + 1 == newBlock.index &&
previousBlock.hash == newBlock.previousHash &&
newBlock.hasValidHash


}
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/com/fluency03/blockchain/core/Blockchain.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.fluency03.blockchain
package core

import com.fluency03.blockchain.core.Block.canBeChained
import com.fluency03.blockchain.core.Block.validLinkBetween
import com.fluency03.blockchain.core.Blockchain._
import org.json4s.native.JsonMethods.{compact, render}
import org.json4s.{Extraction, JValue}
Expand Down Expand Up @@ -51,7 +51,7 @@ object Blockchain {
def isValidChain(chain: Seq[Block]): Boolean = chain match {
case Nil => true
case g +: Nil => g.previousHash == ZERO64 && g.index == 0 && g.hasValidHash
case a +: b +: tail => canBeChained(a, b) && isValidChain(b +: tail)
case a +: b +: tail => validLinkBetween(a, b) && isValidChain(b +: tail)
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,17 @@ object Transaction {
.map(t => t.txOuts.zipWithIndex.map {
case (txOut, index) => Outpoint(t.id, index) -> txOut
}.toMap)
.reduce { _ ++ _ }
.foldLeft(Map.empty[Outpoint, TxOut])(_ ++ _)

def getConsumedUTxOs(transactions: Seq[Transaction]): Map[Outpoint, TxOut] =
transactions.map(_.txIns)
.reduce { _ ++ _ }
.foldLeft(Seq.empty[TxIn])(_ ++ _)
.map(txIn => Outpoint(txIn.previousOut.id, txIn.previousOut.index) -> TxOut("", 0))
.toMap

def noDuplicateTxInOf(transactions: Seq[Transaction]): Boolean = {
val allRefs = transactions.map(_.txIns.map(_.previousOut)).foldLeft(Seq.empty[Outpoint])(_ ++ _)
allRefs.distinct.length == allRefs.length
}

}
4 changes: 4 additions & 0 deletions src/main/scala/com/fluency03/blockchain/core/Wallet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package core
import java.security.KeyPair

import com.fluency03.blockchain.core.Wallet.balanceOfWallet
import com.fluency03.blockchain.core.Transaction.signTxIn

import scala.collection.mutable

Expand All @@ -15,6 +16,9 @@ case class Wallet() {

def balance(uTxOs: mutable.Map[Outpoint, TxOut]): Long = balanceOfWallet(this, uTxOs)

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

}

object Wallet {
Expand Down
2 changes: 2 additions & 0 deletions src/test/scala/com/fluency03/blockchain/CryptoTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class CryptoTest extends FlatSpec with Matchers {
generatePublicKey(pair.getPrivate) shouldEqual pair.getPublic
recoverPublicKey(publicKeyToHex(pair.getPublic)) shouldEqual pair.getPublic
recoverPrivateKey(privateKeyToHex(pair.getPrivate)) shouldEqual pair.getPrivate
recoverPublicKey(pair.getPublic.toHex) shouldEqual pair.getPublic
recoverPrivateKey(pair.getPrivate.toHex) shouldEqual pair.getPrivate
}


Expand Down
38 changes: 37 additions & 1 deletion src/test/scala/com/fluency03/blockchain/core/BlockTest.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.fluency03.blockchain
package core

import java.security.KeyPair

import com.fluency03.blockchain.core.BlockHeader.hashOfBlockHeader
import com.fluency03.blockchain.core.Transaction.createCoinbaseTx
import com.fluency03.blockchain.core.Transaction.{COINBASE_AMOUNT, createCoinbaseTx, signTxIn, updateUTxOs}
import org.json4s.JValue
import org.json4s.JsonAST.{JArray, JInt, JObject, JString}
import org.json4s.JsonDSL._
import org.json4s.native.JsonMethods.parse
import org.scalatest.{FlatSpec, Matchers}

import scala.collection.mutable
import scala.io.Source

class BlockTest extends FlatSpec with Matchers {
Expand All @@ -31,6 +34,7 @@ class BlockTest extends FlatSpec with Matchers {
genesis.nonce shouldEqual expectedHeader.nonce
genesis.hash shouldEqual expectedHeader.hash
genesis.hasValidHash shouldEqual true
genesis.noDuplicateTxIn shouldEqual true
genesis.toJson shouldEqual expectedBlockJson
parse(genesis.toString) shouldEqual expectedBlockJson

Expand Down Expand Up @@ -69,6 +73,7 @@ class BlockTest extends FlatSpec with Matchers {
genesisNextTrial.hash shouldEqual newExpectedHeader.hash
genesisNextTrial.hasValidMerkleHash shouldEqual true
genesisNextTrial.hasValidHash shouldEqual false
genesisNextTrial.noDuplicateTxIn shouldEqual true
val headerJson = expectedHeader.toJson.transformField {
case ("nonce", JInt(x)) => ("nonce", JInt(x+1))
}
Expand Down Expand Up @@ -96,6 +101,7 @@ class BlockTest extends FlatSpec with Matchers {
newBlock.hash shouldEqual newHash
newBlock.hasValidMerkleHash shouldEqual true
newBlock.hasValidHash shouldEqual false
newBlock.noDuplicateTxIn shouldEqual true
val headerJson = expectedHeader.toJson.transformField {
case ("merkleHash", JString(_)) => ("merkleHash", newMerkleHash)
}
Expand Down Expand Up @@ -142,5 +148,35 @@ class BlockTest extends FlatSpec with Matchers {
parse(newBlock.toString) shouldEqual json
}

"allTransAreValid" should "check whether all transactions of a Block are valid." in {
val uTxOs: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]
genesis.allTransAreValid(uTxOs) shouldEqual true

val pair1 = Crypto.generateKeyPair()
val address1 = pair1.getPublic.toHex

val ts = getCurrentTimestamp
val tx: Transaction = Transaction(
Seq(TxIn(Outpoint(genesis.transactions.head.id, 0), "")),
Seq(TxOut(address1, COINBASE_AMOUNT)),
ts)

val nextBlock = Block.mineNextBlock(genesis, "This is next Block!", ts, genesis.difficulty, Seq(tx))
nextBlock.allTransAreValid(uTxOs) shouldEqual false

val uTxOs2 = updateUTxOs(genesis.transactions, uTxOs.toMap)
uTxOs ++= uTxOs2
nextBlock.allTransAreValid(uTxOs) shouldEqual false

val genesisPrivate: String = Source.fromResource("private-key").getLines.mkString
val keyPair = new KeyPair(Crypto.recoverPublicKey(genesisMiner), Crypto.recoverPrivateKey(genesisPrivate))
val signedTxIns = tx.txIns.map(txIn => signTxIn(tx.id.hex2Bytes, txIn, keyPair, uTxOs)).filter(_.isDefined).map(_.get)
signedTxIns.length shouldEqual tx.txIns.length
val signedTx = Transaction(signedTxIns, Seq(TxOut(address1, COINBASE_AMOUNT)), ts)

val validNewBlock = Block.mineNextBlock(genesis, "This is next Block!", ts, genesis.difficulty,
Seq(signedTx, createCoinbaseTx(1, address1, ts)))
validNewBlock.allTransAreValid(uTxOs) shouldEqual true
}

}
37 changes: 37 additions & 0 deletions src/test/scala/com/fluency03/blockchain/core/TransactionTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ class TransactionTest extends FlatSpec with Matchers {
val signedTxIn0 = signTxIn(hash, txIn, pair, uTxOs)
signedTxIn0 shouldEqual None

uTxOs += (Outpoint("def0", 0) -> TxOut("0000", 40))
val signedTxIn1 = signTxIn(hash, txIn, pair, uTxOs)
signedTxIn1 shouldEqual None

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

Expand Down Expand Up @@ -207,4 +211,37 @@ class TransactionTest extends FlatSpec with Matchers {
signedTx2.isValid(uTxOs) shouldEqual true
}

"noDuplicateTxIn" should "detect whether Seq of Transactions contians duplicate TxIns." in {
noDuplicateTxInOf(Seq.empty[Transaction]) shouldEqual true
val tx1: Transaction = Transaction(
Seq(TxIn(Outpoint("a", 0), ""),
TxIn(Outpoint("b", 1), "")),
Seq(TxOut("o", 40)),
genesisTimestamp
)
noDuplicateTxInOf(Seq(tx1)) shouldEqual true
val tx2: Transaction = Transaction(
Seq(TxIn(Outpoint("c", 0), ""),
TxIn(Outpoint("d", 1), "")),
Seq(TxOut("o", 40)),
genesisTimestamp
)
noDuplicateTxInOf(Seq(tx1, tx2)) shouldEqual true
val tx3: Transaction = Transaction(
Seq(TxIn(Outpoint("e", 0), ""),
TxIn(Outpoint("a", 1), "")),
Seq(TxOut("o", 40)),
genesisTimestamp
)
noDuplicateTxInOf(Seq(tx1, tx2, tx3)) shouldEqual true
val tx4: Transaction = Transaction(
Seq(TxIn(Outpoint("a", 0), ""),
TxIn(Outpoint("f", 1), "")),
Seq(TxOut("o", 40)),
genesisTimestamp
)
noDuplicateTxInOf(Seq(tx1, tx2, tx3, tx4)) shouldEqual false
}


}
19 changes: 18 additions & 1 deletion src/test/scala/com/fluency03/blockchain/core/WalletTest.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package com.fluency03.blockchain.core
package com.fluency03.blockchain
package core

import com.fluency03.blockchain.core.Wallet._

Expand All @@ -22,5 +23,21 @@ class WalletTest extends FlatSpec with Matchers {
wallet.balance(uTxOs) shouldEqual 40
}

"Wallet" should "be able to sign a TxIn." in {
val wallet = Wallet()
val id = "".toSha256
val txIn = TxIn(Outpoint("def0", 0), "abc")
val uTxOs: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut]

val signedTxIn0 = wallet.sign(id, txIn, uTxOs)
signedTxIn0 shouldEqual None

uTxOs += (Outpoint("def0", 0) -> TxOut(wallet.address, 40))
uTxOs += (Outpoint("def0", 1) -> TxOut("abc4", 40))

val signedTxIn = wallet.sign(id, txIn, uTxOs)
signedTxIn shouldEqual Some(TxIn(Outpoint("def0", 0), signedTxIn.get.signature))
}


}

0 comments on commit 4dfacec

Please sign in to comment.