mirror of
https://github.com/corda/corda.git
synced 2025-06-01 15:10:54 +00:00
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:
parent
43d18d46bb
commit
ecb1acdb8d
@ -101,6 +101,12 @@ fun PublicKey.toStringShort(): String {
|
|||||||
|
|
||||||
fun Iterable<PublicKey>.toStringsShort(): String = map { it.toStringShort() }.toString()
|
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
|
// Allow Kotlin destructuring: val (private, public) = keyPair
|
||||||
operator fun KeyPair.component1() = this.private
|
operator fun KeyPair.component1() = this.private
|
||||||
|
|
||||||
|
137
core/src/main/kotlin/com/r3corda/core/crypto/PublicKeyTree.kt
Normal file
137
core/src/main/kotlin/com/r3corda/core/crypto/PublicKeyTree.kt
Normal 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()
|
@ -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()) }
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user