Add threshold signature support: introduce PublicKeyTree which allows composing public keys into a tree structure with nodes containing thresholds for how many child node signatures it requires.

This commit is contained in:
Andrius Dagys 2016-10-03 10:20:44 +01:00
parent 43d18d46bb
commit ecb1acdb8d
3 changed files with 192 additions and 0 deletions

View File

@ -101,6 +101,12 @@ fun PublicKey.toStringShort(): String {
fun Iterable<PublicKey>.toStringsShort(): String = map { it.toStringShort() }.toString()
/** Creates a [PublicKeyTree] with a single leaf node containing the public key */
val PublicKey.tree: PublicKeyTree get() = PublicKeyTree.Leaf(this)
/** Returns the set of all [PublicKey]s of the signatures */
fun Iterable<DigitalSignature.WithKey>.byKeys() = map { it.by }.toSet()
// Allow Kotlin destructuring: val (private, public) = keyPair
operator fun KeyPair.component1() = this.private

View File

@ -0,0 +1,137 @@
package com.r3corda.core.crypto
import com.r3corda.core.crypto.PublicKeyTree.Leaf
import com.r3corda.core.crypto.PublicKeyTree.Node
import com.r3corda.core.serialization.deserialize
import com.r3corda.core.serialization.serialize
import java.security.PublicKey
/**
* A tree data structure that enables the representation of composite public keys.
*
* It the simplest case it may just contain a single node encapsulating a [PublicKey] a [Leaf].
*
* For more complex scenarios, such as *"Both Alice and Bob need to sign to consume a sate S"*, we can represent
* the requirement by creating a tree with a root [Node], and Alice and Bob as children [Leaf]s.
* The root node would specify *weights* for each of its children and a *threshold* the minimum total weight required
* (e.g. the minimum number of child signatures required) to satisfy the tree signature requirement.
*
* Using these constructs we can express e.g. 1 of N (OR) or N of N (AND) signature requirements. By nesting we can
* create multi-level requirements such as *"either the CEO or 3 of 5 of his assistants need to sign"*.
*/
sealed class PublicKeyTree {
/** Checks whether [keys] match a sufficient amount of leaf nodes */
abstract fun isFulfilledBy(keys: Iterable<PublicKey>): Boolean
fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key))
/** Returns all [PublicKey]s contained within the tree leaves */
abstract fun getKeys(): Set<PublicKey>
/** Checks whether any of the given [keys] matches a leaf on the tree */
fun containsAny(keys: Iterable<PublicKey>) = getKeys().intersect(keys).isNotEmpty()
// TODO: implement a proper encoding/decoding mechanism
fun toBase58String(): String = Base58.encode(this.serialize().bits)
companion object {
fun parseFromBase58(encoded: String) = Base58.decode(encoded).deserialize<PublicKeyTree>()
}
/** The leaf node of the public key tree a wrapper around a [PublicKey] primitive */
class Leaf(val publicKey: PublicKey) : PublicKeyTree() {
override fun isFulfilledBy(keys: Iterable<PublicKey>) = publicKey in keys
override fun getKeys(): Set<PublicKey> = setOf(publicKey)
// Auto-generated. TODO: remove once data class inheritance is enabled
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as Leaf
if (publicKey != other.publicKey) return false
return true
}
override fun hashCode() = publicKey.hashCode()
}
/**
* Represents a node in the [PublicKeyTree]. It maintains a list of child nodes sub-trees, and associated
* [weights] carried by child node signatures.
*
* The [threshold] specifies the minimum total weight required (in the simple case the minimum number of child
* signatures required) to satisfy the public key sub-tree rooted at this node.
*/
class Node(val threshold: Int,
val children: List<PublicKeyTree>,
val weights: List<Int>) : PublicKeyTree() {
override fun isFulfilledBy(keys: Iterable<PublicKey>): Boolean {
val totalWeight = children.mapIndexed { i, childNode ->
if (childNode.isFulfilledBy(keys)) weights[i] else 0
}.sum()
return totalWeight >= threshold
}
override fun getKeys(): Set<PublicKey> = children.flatMap { it.getKeys() }.toSet()
// Auto-generated. TODO: remove once data class inheritance is enabled
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as Node
if (threshold != other.threshold) return false
if (weights != other.weights) return false
if (children != other.children) return false
return true
}
override fun hashCode(): Int {
var result = threshold
result = 31 * result + weights.hashCode()
result = 31 * result + children.hashCode()
return result
}
}
/** A helper class for building a [PublicKeyTree.Node]. */
class Builder() {
private val children: MutableList<PublicKeyTree> = mutableListOf()
private val weights: MutableList<Int> = mutableListOf()
/** Adds a child [PublicKeyTree] node. Specifying a [weight] for the child is optional and will default to 1. */
fun addKey(publicKey: PublicKeyTree, weight: Int = 1): Builder {
children.add(publicKey)
weights.add(weight)
return this
}
fun addKeys(vararg publicKeys: PublicKeyTree): Builder {
publicKeys.forEach { addKey(it) }
return this
}
fun addLeaves(publicKeys: List<PublicKey>): Builder = addLeaves(*publicKeys.toTypedArray())
fun addLeaves(vararg publicKeys: PublicKey) = addKeys(*publicKeys.map { it.tree }.toTypedArray())
/**
* Builds the [PublicKeyTree.Node]. If [threshold] is not specified, it will default to
* the size of the children, effectively generating an "N of N" requirement.
*/
fun build(threshold: Int? = null): PublicKeyTree {
return if (children.size == 1) children.first()
else Node(threshold ?: children.size, children.toList(), weights.toList())
}
}
}
/** Returns the set of all [PublicKey]s contained in the leaves of the [PublicKeyTree]s */
fun Iterable<PublicKeyTree>.getKeys() = flatMap { it.getKeys() }.toSet()

View File

@ -0,0 +1,49 @@
package com.r3corda.core.crypto
import com.r3corda.core.serialization.OpaqueBytes
import org.junit.Test
import kotlin.test.assertTrue
class PublicKeyTreeTests {
val aliceKey = generateKeyPair()
val bobKey = generateKeyPair()
val charlieKey = generateKeyPair()
val alicePublicKey = PublicKeyTree.Leaf(aliceKey.public)
val bobPublicKey = PublicKeyTree.Leaf(bobKey.public)
val charliePublicKey = PublicKeyTree.Leaf(charlieKey.public)
val message = OpaqueBytes("Transaction".toByteArray())
val aliceSignature = aliceKey.signWithECDSA(message)
val bobSignature = bobKey.signWithECDSA(message)
@Test
fun `(Alice) fulfilled by Alice signature`() {
assertTrue { alicePublicKey.isFulfilledBy(aliceSignature.by) }
}
@Test
fun `(Alice or Bob) fulfilled by Bob signature`() {
val aliceOrBob = PublicKeyTree.Builder().addKeys(alicePublicKey, bobPublicKey).build(threshold = 1)
assertTrue { aliceOrBob.isFulfilledBy(bobSignature.by) }
}
@Test
fun `(Alice and Bob) fulfilled by Alice, Bob signatures`() {
val aliceAndBob = PublicKeyTree.Builder().addKeys(alicePublicKey, bobPublicKey).build()
val signatures = listOf(aliceSignature, bobSignature)
assertTrue { aliceAndBob.isFulfilledBy(signatures.byKeys()) }
}
@Test
fun `((Alice and Bob) or Charlie) signature verifies`() {
// TODO: Look into a DSL for building multi-level public key trees if that becomes a common use case
val aliceAndBob = PublicKeyTree.Builder().addKeys(alicePublicKey, bobPublicKey).build()
val aliceAndBobOrCharlie = PublicKeyTree.Builder().addKeys(aliceAndBob, charliePublicKey).build(threshold = 1)
val signatures = listOf(aliceSignature, bobSignature)
assertTrue { aliceAndBobOrCharlie.isFulfilledBy(signatures.byKeys()) }
}
}