Tearoff fixes (#78)

* Move merkle building extension functions on wire tx to WireTransaction class.

* Add timestamp, notary, transaction type and signers to wire transaction id calculation.

* Change construction of MerkleTree from duplicating last node on a given level to padding leaves' list with zero hash to size of the nearest power of 2 - so we always have a full binary tree.
The problem was that it was possible to construct 2 different transactions with the same ids. Trick worked for txs having number of leaves that were not power of 2.

* Update tear-offs documentation and diagrams to reflect changes in construction of Merkle trees - padding with zero hashes and including all WireTransaction fields in id computation.

* Change in filtering API of WireTransaction for partial Merkle trees calculation.
Instead of many filtering functions over a transaction only one needs to be provided.
Additional change to check and verification of FilteredTransaction.

* IRS demo change. Make filtering function a protected method of RatesFixFlow class.
Comment on situation when capturing too much scope and connected problems with checkpointing.
Change oracle and tear-offs documentation.
This commit is contained in:
kasiastreich
2017-02-03 14:02:51 +00:00
committed by GitHub
parent b86c80691e
commit 45d8e0f76d
13 changed files with 409 additions and 230 deletions

View File

@ -1,7 +1,7 @@
package net.corda.core.crypto
import net.corda.core.transactions.MerkleTree
import net.corda.core.transactions.hashConcat
import net.corda.core.crypto.SecureHash.Companion.zeroHash
import java.util.*
@ -19,14 +19,12 @@ class MerkleTreeException(val reason: String) : Exception() {
* / \
* h14 h55
* / \ / \
* h12 h34 h5->d(h5)
* / \ / \ / \
* l1 l2 l3 l4 l5->d(l5)
* h12 h34 h50 h00
* / \ / \ / \ / \
* l1 l2 l3 l4 l5 0 0 0
*
* l* denote hashes of leaves, h* - hashes of nodes below.
* h5->d(h5) denotes duplication of the left hand side node. These nodes are kept in a full tree as DuplicatedLeaf.
* When filtering the tree for l5, we don't want to keep both l5 and its duplicate (it can also be solved using null
* values in a tree, but this solution is clearer).
* l* denote hashes of leaves, h* - hashes of nodes below. 0 denotes zero hash, we use it to pad not full binary trees,
* so the number of leaves is always a power of 2.
*
* Example of Partial tree based on the tree above.
*
@ -34,13 +32,13 @@ class MerkleTreeException(val reason: String) : Exception() {
* / \
* _ _
* / \ / \
* h12 _ _ d(h5)
* h12 _ _ h00
* / \ / \
* I3 l4 I5 d(l5)
* I3 l4 I5 0
*
* We want to check l3 and l5 - now turned into IncudedLeaf (I3 and I5 above). To verify that these two leaves belong to
* the tree with a hash root h15 we need to provide a Merkle branch (or partial tree). In our case we need hashes:
* h12, l4, d(l5) and d(h5). Verification is done by hashing the partial tree to obtain the root and checking it against
* h12, l4, 0 and h00. Verification is done by hashing the partial tree to obtain the root and checking it against
* the obtained h15 hash. Additionally we store included hashes used in calculation and compare them to leaves hashes we got
* (there can be a difference in obtained leaves ordering - that's why it's a set comparison not hashing leaves into a tree).
* If both equalities hold, we can assume that l3 and l5 belong to the transaction with root h15.
@ -54,7 +52,7 @@ class PartialMerkleTree(val root: PartialTree) {
* transaction and leaves that just keep hashes needed for calculation. Reason for this approach: during verification
* it's easier to extract hashes used as a base for this tree.
*/
sealed class PartialTree() {
sealed class PartialTree {
class IncludedLeaf(val hash: SecureHash) : PartialTree()
class Leaf(val hash: SecureHash) : PartialTree()
class Node(val left: PartialTree, val right: PartialTree) : PartialTree()
@ -66,15 +64,31 @@ class PartialMerkleTree(val root: PartialTree) {
* @param includeHashes Hashes that should be included in a partial tree.
* @return Partial Merkle tree root.
*/
@Throws(IllegalArgumentException::class, MerkleTreeException::class)
fun build(merkleRoot: MerkleTree, includeHashes: List<SecureHash>): PartialMerkleTree {
val usedHashes = ArrayList<SecureHash>()
require(zeroHash !in includeHashes) { "Zero hashes shouldn't be included in partial tree." }
checkFull(merkleRoot) // Throws MerkleTreeException if it is not a full binary tree.
val tree = buildPartialTree(merkleRoot, includeHashes, usedHashes)
//Too much included hashes or different ones.
// Too many included hashes or different ones.
if (includeHashes.size != usedHashes.size)
throw MerkleTreeException("Some of the provided hashes are not in the tree.")
return PartialMerkleTree(tree.second)
}
// Check if a MerkleTree is full binary tree. Returns the height of the tree if full, otherwise throws exception.
private fun checkFull(tree: MerkleTree, level: Int = 0): Int {
return when (tree) {
is MerkleTree.Leaf -> level
is MerkleTree.Node -> {
val l1 = checkFull(tree.left, level+1)
val l2 = checkFull(tree.right, level+1)
if (l1 != l2) throw MerkleTreeException("Got not full binary tree.")
l1
}
}
}
/**
* @param root Root of full Merkle tree which is a base for a partial one.
* @param includeHashes Hashes of leaves to be included in this partial tree.
@ -93,18 +107,17 @@ class PartialMerkleTree(val root: PartialTree) {
usedHashes.add(root.value)
Pair(true, PartialTree.IncludedLeaf(root.value))
} else Pair(false, PartialTree.Leaf(root.value))
is MerkleTree.DuplicatedLeaf -> Pair(false, PartialTree.Leaf(root.value))
is MerkleTree.Node -> {
val leftNode = buildPartialTree(root.left, includeHashes, usedHashes)
val rightNode = buildPartialTree(root.right, includeHashes, usedHashes)
if (leftNode.first or rightNode.first) {
//This node is on a path to some included leaves. Don't store hash.
// This node is on a path to some included leaves. Don't store hash.
val newTree = PartialTree.Node(leftNode.second, rightNode.second)
return Pair(true, newTree)
Pair(true, newTree)
} else {
//This node has no included leaves below. Cut the tree here and store a hash as a Leaf.
// This node has no included leaves below. Cut the tree here and store a hash as a Leaf.
val newTree = PartialTree.Leaf(root.value)
return Pair(false, newTree)
Pair(false, newTree)
}
}
}
@ -118,7 +131,7 @@ class PartialMerkleTree(val root: PartialTree) {
fun verify(merkleRootHash: SecureHash, hashesToCheck: List<SecureHash>): Boolean {
val usedHashes = ArrayList<SecureHash>()
val verifyRoot = verify(root, usedHashes)
//It means that we obtained more/less hashes than needed or different sets of hashes.
// It means that we obtained more/fewer hashes than needed or different sets of hashes.
if (hashesToCheck.groupBy { it } != usedHashes.groupBy { it })
return false
return (verifyRoot == merkleRootHash)

View File

@ -19,6 +19,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
override fun toString() = BaseEncoding.base16().encode(bytes)
fun prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen)
fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256()
// Like static methods in Java, except the 'companion' is a singleton that can have state.
companion object {
@ -35,6 +36,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
@JvmStatic fun sha256(str: String) = sha256(str.toByteArray())
@JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32))
val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() }))
}
// In future, maybe SHA3, truncated hashes etc.

View File

@ -1,39 +1,15 @@
package net.corda.core.transactions
import net.corda.core.contracts.Command
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.MerkleTreeException
import net.corda.core.crypto.PartialMerkleTree
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.crypto.SecureHash.Companion.zeroHash
import net.corda.core.serialization.createKryo
import net.corda.core.serialization.extendKryoHash
import net.corda.core.serialization.serialize
import java.util.*
/**
* Build filtered transaction using provided filtering functions.
*/
fun WireTransaction.buildFilteredTransaction(filterFuns: FilterFuns): FilteredTransaction {
return FilteredTransaction.buildMerkleTransaction(this, filterFuns)
}
/**
* Calculation of all leaves hashes that are needed for calculation of transaction id and partial Merkle branches.
*/
fun WireTransaction.calculateLeavesHashes(): List<SecureHash> {
val resultHashes = ArrayList<SecureHash>()
val entries = listOf(inputs, outputs, attachments, commands)
entries.forEach { it.mapTo(resultHashes, { x -> serializedHash(x) }) }
return resultHashes
}
fun SecureHash.hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256()
fun <T : Any> serializedHash(x: T): SecureHash {
val kryo = extendKryoHash(createKryo()) //Dealing with HashMaps inside states.
val kryo = extendKryoHash(createKryo()) // Dealing with HashMaps inside states.
return x.serialize(kryo).hash
}
@ -42,55 +18,58 @@ fun <T : Any> serializedHash(x: T): SecureHash {
*
* See: https://en.wikipedia.org/wiki/Merkle_tree
*
* Transaction is split into following blocks: inputs, outputs, commands, attachments' refs. Merkle Tree is kept in
* a recursive data structure. Building is done bottom up, from all leaves' hashes.
* If a row in a tree has an odd number of elements - the final hash is hashed with itself.
* Transaction is split into following blocks: inputs, attachments' refs, outputs, commands, notary,
* signers, tx type, timestamp. Merkle Tree is kept in a recursive data structure. Building is done bottom up,
* from all leaves' hashes. If number of leaves is not a power of two, the tree is padded with zero hashes.
*/
sealed class MerkleTree(val hash: SecureHash) {
class Leaf(val value: SecureHash) : MerkleTree(value)
class Node(val value: SecureHash, val left: MerkleTree, val right: MerkleTree) : MerkleTree(value)
//DuplicatedLeaf is storing a hash of the rightmost node that had to be duplicated to obtain the tree.
//That duplication can cause problems while building and verifying partial tree (especially for trees with duplicate
//attachments or commands).
class DuplicatedLeaf(val value: SecureHash) : MerkleTree(value)
fun hashNodes(right: MerkleTree): MerkleTree {
val newHash = this.hash.hashConcat(right.hash)
return Node(newHash, this, right)
}
companion object {
private fun isPow2(num: Int): Boolean = num and (num-1) == 0
/**
* Merkle tree building using hashes.
* Merkle tree building using hashes, with zero hash padding to full power of 2.
*/
@Throws(IllegalArgumentException::class)
fun getMerkleTree(allLeavesHashes: List<SecureHash>): MerkleTree {
val leaves = allLeavesHashes.map { MerkleTree.Leaf(it) }
val leaves = padWithZeros(allLeavesHashes).map { MerkleTree.Leaf(it) }
return buildMerkleTree(leaves)
}
// If number of leaves in the tree is not a power of 2, we need to pad it with zero hashes.
private fun padWithZeros(allLeavesHashes: List<SecureHash>): List<SecureHash> {
var n = allLeavesHashes.size
if (isPow2(n)) return allLeavesHashes
val paddedHashes = ArrayList<SecureHash>(allLeavesHashes)
while (!isPow2(n)) {
paddedHashes.add(zeroHash)
n++
}
return paddedHashes
}
/**
* Tailrecursive function for building a tree bottom up.
* @param lastNodesList MerkleTree nodes from previous level.
* @return Tree root.
*/
private tailrec fun buildMerkleTree(lastNodesList: List<MerkleTree>): MerkleTree {
if (lastNodesList.size < 1)
if (lastNodesList.isEmpty())
throw MerkleTreeException("Cannot calculate Merkle root on empty hash list.")
if (lastNodesList.size == 1) {
return lastNodesList[0] //Root reached.
} else {
val newLevelHashes: MutableList<MerkleTree> = ArrayList()
var i = 0
while (i < lastNodesList.size) {
val n = lastNodesList.size
while (i < n) {
val left = lastNodesList[i]
val n = lastNodesList.size
// If there is an odd number of elements at this level,
// the last element is hashed with itself and stored as a Leaf.
val right = when {
i + 1 > n - 1 -> MerkleTree.DuplicatedLeaf(lastNodesList[n - 1].hash)
else -> lastNodesList[i + 1]
}
val combined = left.hashNodes(right)
require(i+1 <= n-1) { "Sanity check: number of nodes should be even." }
val right = lastNodesList[i+1]
val newHash = left.hash.hashConcat(right.hash)
val combined = Node(newHash, left, right)
newLevelHashes.add(combined)
i += 2
}
@ -101,40 +80,67 @@ sealed class MerkleTree(val hash: SecureHash) {
}
/**
* Class that holds filtered leaves for a partial Merkle transaction. We assume mixed leaves types.
* Interface implemented by WireTransaction and FilteredLeaves.
* Property traversableList assures that we always calculate hashes in the same order, lets us define which
* fields of WireTransaction will be included in id calculation or partial merkle tree building.
*/
class FilteredLeaves(
val inputs: List<StateRef>,
val outputs: List<TransactionState<ContractState>>,
val attachments: List<SecureHash>,
val commands: List<Command>
) {
fun getFilteredHashes(): List<SecureHash> {
val resultHashes = ArrayList<SecureHash>()
val entries = listOf(inputs, outputs, attachments, commands)
entries.forEach { it.mapTo(resultHashes, { x -> serializedHash(x) }) }
return resultHashes
}
interface TraversableTransaction {
val inputs: List<StateRef>
val attachments: List<SecureHash>
val outputs: List<TransactionState<ContractState>>
val commands: List<Command>
val notary: Party?
val mustSign: List<CompositeKey>
val type: TransactionType?
val timestamp: Timestamp?
/**
* Traversing transaction fields with a list function over transaction contents. Used for leaves hashes calculation
* and user provided filtering and checking of filtered transaction.
*/
// We may want to specify our own behaviour on certain tx fields.
// Like if we include them at all, what to do with null values, if we treat list as one or not etc. for building
// torn-off transaction and id calculation.
val traversableList: List<Any>
get() {
val traverseList = mutableListOf(inputs, attachments, outputs, commands).flatten().toMutableList()
if (notary != null) traverseList.add(notary!!)
traverseList.addAll(mustSign)
if (type != null) traverseList.add(type!!)
if (timestamp != null) traverseList.add(timestamp!!)
return traverseList
}
// Calculation of all leaves hashes that are needed for calculation of transaction id and partial Merkle branches.
fun calculateLeavesHashes(): List<SecureHash> = traversableList.map { serializedHash(it) }
}
/**
* Holds filter functions on transactions fields.
* Functions are used to build a partial tree only out of some subset of original transaction fields.
* Class that holds filtered leaves for a partial Merkle transaction. We assume mixed leaf types, notice that every
* field from WireTransaction can be used in PartialMerkleTree calculation.
*/
class FilterFuns(
val filterInputs: (StateRef) -> Boolean = { false },
val filterOutputs: (TransactionState<ContractState>) -> Boolean = { false },
val filterAttachments: (SecureHash) -> Boolean = { false },
val filterCommands: (Command) -> Boolean = { false }
) {
fun <T : Any> genericFilter(elem: T): Boolean {
return when (elem) {
is StateRef -> filterInputs(elem)
is TransactionState<*> -> filterOutputs(elem)
is SecureHash -> filterAttachments(elem)
is Command -> filterCommands(elem)
else -> throw IllegalArgumentException("Wrong argument type: ${elem.javaClass}")
}
class FilteredLeaves(
override val inputs: List<StateRef>,
override val attachments: List<SecureHash>,
override val outputs: List<TransactionState<ContractState>>,
override val commands: List<Command>,
override val notary: Party?,
override val mustSign: List<CompositeKey>,
override val type: TransactionType?,
override val timestamp: Timestamp?
) : TraversableTransaction {
/**
* Function that checks the whole filtered structure.
* Force type checking on a structure that we obtained, so we don't sign more than expected.
* Example: Oracle is implemented to check only for commands, if it gets an attachment and doesn't expect it - it can sign
* over a transaction with the attachment that wasn't verified. Of course it depends on how you implement it, but else -> false
* should solve a problem with possible later extensions to WireTransaction.
* @param checkingFun function that performs type checking on the structure fields and provides verification logic accordingly.
* @returns false if no elements were matched on a structure or checkingFun returned false.
*/
fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean {
val checkList = traversableList.map { checkingFun(it) }
return (!checkList.isEmpty()) && checkList.all { true }
}
}
@ -151,18 +157,14 @@ class FilteredTransaction(
/**
* Construction of filtered transaction with Partial Merkle Tree.
* @param wtx WireTransaction to be filtered.
* @param filterFuns filtering functions for inputs, outputs, attachments, commands.
* @param filtering filtering over the whole WireTransaction
*/
fun buildMerkleTransaction(wtx: WireTransaction,
filterFuns: FilterFuns
filtering: (Any) -> Boolean
): FilteredTransaction {
val filteredInputs = wtx.inputs.filter { filterFuns.genericFilter(it) }
val filteredOutputs = wtx.outputs.filter { filterFuns.genericFilter(it) }
val filteredAttachments = wtx.attachments.filter { filterFuns.genericFilter(it) }
val filteredCommands = wtx.commands.filter { filterFuns.genericFilter(it) }
val filteredLeaves = FilteredLeaves(filteredInputs, filteredOutputs, filteredAttachments, filteredCommands)
val pmt = PartialMerkleTree.build(wtx.merkleTree, filteredLeaves.getFilteredHashes())
val filteredLeaves = wtx.filterWithFun(filtering)
val merkleTree = wtx.getMerkleTree()
val pmt = PartialMerkleTree.build(merkleTree, filteredLeaves.calculateLeavesHashes())
return FilteredTransaction(filteredLeaves, pmt)
}
}
@ -170,10 +172,19 @@ class FilteredTransaction(
/**
* Runs verification of Partial Merkle Branch with merkleRootHash.
*/
@Throws(MerkleTreeException::class)
fun verify(merkleRootHash: SecureHash): Boolean {
val hashes: List<SecureHash> = filteredLeaves.getFilteredHashes()
if (hashes.size == 0)
val hashes: List<SecureHash> = filteredLeaves.calculateLeavesHashes()
if (hashes.isEmpty())
throw MerkleTreeException("Transaction without included leaves.")
return partialMerkleTree.verify(merkleRootHash, hashes)
}
/**
* Runs verification of Partial Merkle Branch with merkleRootHash. Checks filteredLeaves with provided checkingFun.
*/
@Throws(MerkleTreeException::class)
fun verifyWithFunction(merkleRootHash: SecureHash, checkingFun: (Any) -> Boolean): Boolean {
return verify(merkleRootHash) && filteredLeaves.checkWithFun { checkingFun(it) }
}
}

View File

@ -25,15 +25,15 @@ class WireTransaction(
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
override val inputs: List<StateRef>,
/** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */
val attachments: List<SecureHash>,
override val attachments: List<SecureHash>,
outputs: List<TransactionState<ContractState>>,
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
val commands: List<Command>,
override val commands: List<Command>,
notary: Party?,
signers: List<CompositeKey>,
type: TransactionType,
timestamp: Timestamp?
) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp) {
) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp), TraversableTransaction {
init {
checkInvariants()
}
@ -42,14 +42,7 @@ class WireTransaction(
@Volatile @Transient private var cachedBytes: SerializedBytes<WireTransaction>? = null
val serialized: SerializedBytes<WireTransaction> get() = cachedBytes ?: serialize().apply { cachedBytes = this }
//We need cashed leaves hashes and whole tree for an id and Partial Merkle Tree calculation.
@Volatile @Transient private var cachedLeavesHashes: List<SecureHash>? = null
val allLeavesHashes: List<SecureHash> get() = cachedLeavesHashes ?: calculateLeavesHashes().apply { cachedLeavesHashes = this }
@Volatile @Transient var cachedTree: MerkleTree? = null
val merkleTree: MerkleTree get() = cachedTree ?: MerkleTree.getMerkleTree(allLeavesHashes).apply { cachedTree = this }
override val id: SecureHash get() = merkleTree.hash
override val id: SecureHash by lazy { getMerkleTree().hash }
companion object {
fun deserialize(data: SerializedBytes<WireTransaction>, kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction {
@ -91,6 +84,39 @@ class WireTransaction(
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, mustSign, timestamp, type)
}
/**
* Build filtered transaction using provided filtering functions.
*/
fun buildFilteredTransaction(filtering: (Any) -> Boolean): FilteredTransaction {
return FilteredTransaction.buildMerkleTransaction(this, filtering)
}
/**
* Builds whole Merkle tree for a transaction.
*/
fun getMerkleTree(): MerkleTree {
return MerkleTree.getMerkleTree(calculateLeavesHashes())
}
/**
* Construction of partial transaction from WireTransaction based on filtering.
* @param filtering filtering over the whole WireTransaction
* @returns FilteredLeaves used in PartialMerkleTree calculation and verification.
*/
fun filterWithFun(filtering: (Any) -> Boolean): FilteredLeaves {
fun notNullFalse(elem: Any?): Any? = if(elem == null || !filtering(elem)) null else elem
return FilteredLeaves(
inputs.filter { filtering(it) },
attachments.filter { filtering(it) },
outputs.filter { filtering(it) },
commands.filter { filtering(it) },
notNullFalse(notary) as Party?,
mustSign.filter { filtering(it) },
notNullFalse(type) as TransactionType?,
notNullFalse(timestamp) as Timestamp?
)
}
override fun toString(): String {
val buf = StringBuilder()
buf.appendln("Transaction $id:")

View File

@ -3,26 +3,22 @@ package net.corda.core.crypto
import com.esotericsoftware.kryo.serializers.MapSerializer
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.`issued by`
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash.Companion.zeroHash
import net.corda.core.serialization.*
import net.corda.core.transactions.*
import net.corda.core.utilities.DUMMY_PUBKEY_1
import net.corda.testing.ALICE_PUBKEY
import net.corda.core.utilities.*
import net.corda.testing.MEGA_CORP
import net.corda.testing.MEGA_CORP_PUBKEY
import net.corda.testing.ledger
import org.junit.Test
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.test.*
class PartialMerkleTreeTest {
val nodes = "abcdef"
val hashed = nodes.map { it.serialize().sha256() }
val root = SecureHash.parse("F6D8FB3720114F8D040D64F633B0D9178EB09A55AA7D62FAE1A070D1BF561051")
val expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash)).hash
val merkleTree = MerkleTree.getMerkleTree(hashed)
val testLedger = ledger {
@ -33,22 +29,30 @@ class PartialMerkleTreeTest {
owner = MEGA_CORP_PUBKEY
)
}
output("dummy cash 1") {
Cash.State(
amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
owner = DUMMY_PUBKEY_1
)
}
}
transaction {
input("MEGA_CORP cash")
output("MEGA_CORP cash".output<Cash.State>().copy(owner = DUMMY_PUBKEY_1))
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
timestamp(TEST_TX_TIME)
this.verifies()
}
}
val testTx = testLedger.interpreter.transactionsToVerify[0]
val txs = testLedger.interpreter.transactionsToVerify
val testTx = txs[0]
//Building full Merkle Tree tests.
// Building full Merkle Tree tests.
@Test
fun `building Merkle tree with 6 nodes - no rightmost nodes`() {
assertEquals(root, merkleTree.hash)
assertEquals(expectedRoot, merkleTree.hash)
}
@Test
@ -67,25 +71,70 @@ class PartialMerkleTreeTest {
fun `building Merkle tree odd number of nodes`() {
val odd = hashed.subList(0, 3)
val h1 = hashed[0].hashConcat(hashed[1])
val h2 = hashed[2].hashConcat(hashed[2])
val h2 = hashed[2].hashConcat(zeroHash)
val expected = h1.hashConcat(h2)
val mt = MerkleTree.getMerkleTree(odd)
assertEquals(mt.hash, expected)
}
@Test
fun `check full tree`() {
val h = SecureHash.randomSHA256()
val left = MerkleTree.Node(h, MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h)),
MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h)))
val right = MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h))
val tree = MerkleTree.Node(h, left, right)
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(tree, listOf(h)) }
PartialMerkleTree.build(right, listOf(h, h)) // Node and two leaves.
PartialMerkleTree.build(MerkleTree.Leaf(h), listOf(h)) // Just a leaf.
}
@Test
fun `building Merkle tree for a transaction`() {
val filterFuns = FilterFuns(
filterCommands = { x -> ALICE_PUBKEY in x.signers },
filterOutputs = { true },
filterInputs = { true })
val mt = testTx.buildFilteredTransaction(filterFuns)
fun filtering(elem: Any): Boolean {
return when (elem) {
is StateRef -> true
is TransactionState<*> -> elem.data.participants[0].keys == DUMMY_PUBKEY_1.keys
is Command -> MEGA_CORP_PUBKEY in elem.signers
is Timestamp -> true
is CompositeKey -> elem == MEGA_CORP_PUBKEY
else -> false
}
}
val mt = testTx.buildFilteredTransaction(::filtering)
val leaves = mt.filteredLeaves
val d = WireTransaction.deserialize(testTx.serialized)
assertEquals(testTx.id, d.id)
assertEquals(1, leaves.commands.size)
assertEquals(1, leaves.outputs.size)
assertEquals(1, leaves.inputs.size)
assertEquals(1, leaves.mustSign.size)
assertEquals(0, leaves.attachments.size)
assertTrue(mt.filteredLeaves.timestamp != null)
assertEquals(null, mt.filteredLeaves.type)
assertEquals(null, mt.filteredLeaves.notary)
assert(mt.verify(testTx.id))
}
//Partial Merkle Tree building tests
@Test
fun `same transactions with different notaries have different ids`() {
val wtx1 = makeSimpleCashWtx(DUMMY_NOTARY)
val wtx2 = makeSimpleCashWtx(MEGA_CORP)
assertNotEquals(wtx1.id, wtx2.id)
}
@Test
fun `nothing filtered`() {
val mt = testTx.buildFilteredTransaction( {false} )
assertTrue(mt.filteredLeaves.attachments.isEmpty())
assertTrue(mt.filteredLeaves.commands.isEmpty())
assertTrue(mt.filteredLeaves.inputs.isEmpty())
assertTrue(mt.filteredLeaves.outputs.isEmpty())
assertTrue(mt.filteredLeaves.timestamp == null)
assertFailsWith<MerkleTreeException> { mt.verify(testTx.id) }
}
// Partial Merkle Tree building tests
@Test
fun `build Partial Merkle Tree, only left nodes branch`() {
val inclHashes = listOf(hashed[3], hashed[5])
@ -137,7 +186,7 @@ class PartialMerkleTreeTest {
@Test
fun `verify Partial Merkle Tree - duplicate leaves failure`() {
val mt = MerkleTree.getMerkleTree(hashed.subList(0, 5)) //Odd number of leaves. Last one is duplicated.
val mt = MerkleTree.getMerkleTree(hashed.subList(0, 5)) // Odd number of leaves. Last one is duplicated.
val inclHashes = arrayListOf(hashed[3], hashed[4])
val pmt = PartialMerkleTree.build(mt, inclHashes)
inclHashes.add(hashed[4])
@ -162,11 +211,24 @@ class PartialMerkleTreeTest {
@Test
fun `hash map serialization`() {
val hm1 = hashMapOf("a" to 1, "b" to 2, "c" to 3, "e" to 4)
assert(serializedHash(hm1) == serializedHash(hm1.serialize().deserialize())) //It internally uses the ordered HashMap extension.
assert(serializedHash(hm1) == serializedHash(hm1.serialize().deserialize())) // It internally uses the ordered HashMap extension.
val kryo = extendKryoHash(createKryo())
assertTrue(kryo.getSerializer(HashMap::class.java) is OrderedSerializer)
assertTrue(kryo.getSerializer(LinkedHashMap::class.java) is MapSerializer)
val hm2 = hm1.serialize(kryo).deserialize(kryo)
assert(hm1.hashCode() == hm2.hashCode())
}
private fun makeSimpleCashWtx(notary: Party, timestamp: Timestamp? = null, attachments: List<SecureHash> = emptyList()): WireTransaction {
return WireTransaction(
inputs = testTx.inputs,
attachments = attachments,
outputs = testTx.outputs,
commands = testTx.commands,
notary = notary,
signers = listOf(MEGA_CORP_PUBKEY, DUMMY_PUBKEY_1),
type = TransactionType.General(),
timestamp = timestamp
)
}
}