Major: introduce TransactionGroup for verifying subgraphs.

This commit is contained in:
Mike Hearn 2015-11-18 18:55:02 +01:00
parent 636279ced9
commit a4aef06e41
6 changed files with 268 additions and 37 deletions

View File

@ -25,7 +25,7 @@ fun ContractState.hash(): SecureHash = SecureHash.sha256((serialize()))
* A stateref is a pointer to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which transaction
* defined the state and where in that transaction it was.
*/
data class ContractStateRef(val txhash: SecureHash.SHA256, val index: Int) : SerializeableWithKryo
data class ContractStateRef(val txhash: SecureHash, val index: Int) : SerializeableWithKryo
/** A StateAndRef is simply a (state, ref) pair. For instance, a wallet (which holds available assets) contains these. */
data class StateAndRef<T : ContractState>(val state: T, val ref: ContractStateRef)

View File

@ -0,0 +1,58 @@
package core
import java.util.*
class TransactionResolutionException(val hash: SecureHash) : Exception()
class TransactionConflictException(val conflictRef: ContractStateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
/**
* A TransactionGroup defines a directed acyclic graph of transactions that can be resolved with each other and then
* verified. Successful verification does not imply the non-existence of other conflicting transactions: simply that
* this subgraph does not contain conflicts and is accepted by the involved contracts.
*
* The inputs of the provided transactions must be resolvable either within the [transactions] set, or from the
* [nonVerifiedRoots] set. Transactions in the non-verified set are ignored other than for looking up input states.
*/
class TransactionGroup(val transactions: Set<LedgerTransaction>, val nonVerifiedRoots: Set<LedgerTransaction>) {
/**
* Verifies the group and returns the set of resolved transactions.
*/
fun verify(programMap: Map<SecureHash, Contract>): Set<TransactionForVerification> {
// Check that every input can be resolved to an output.
// Check that no output is referenced by more than one input.
// Cycles should be impossible due to the use of hashes as pointers.
check(transactions.intersect(nonVerifiedRoots).isEmpty())
val hashToTXMap: Map<SecureHash, List<LedgerTransaction>> = (transactions + nonVerifiedRoots).groupBy { it.hash }
val refToConsumingTXMap = hashMapOf<ContractStateRef, LedgerTransaction>()
val resolved = HashSet<TransactionForVerification>(transactions.size)
for (tx in transactions) {
val inputs = ArrayList<ContractState>(tx.inStateRefs.size)
for (ref in tx.inStateRefs) {
val conflict = refToConsumingTXMap[ref]
if (conflict != null)
throw TransactionConflictException(ref, tx, conflict)
refToConsumingTXMap[ref] = tx
// Look up the connecting transaction.
val ltx = hashToTXMap[ref.txhash]?.single() ?: throw TransactionResolutionException(ref.txhash)
// Look up the output in that transaction by index.
inputs.add(ltx.outStates[ref.index])
}
resolved.add(TransactionForVerification(inputs, tx.outStates, tx.commands, tx.time, tx.hash))
}
for (tx in resolved) {
try {
tx.verify(programMap)
} catch(e: Exception) {
println(tx)
throw e
}
}
return resolved
}
}

View File

@ -170,7 +170,7 @@ data class TimestampedWireTransaction(
*
* TODO: When converting LedgerTransaction into TransactionForVerification, make sure to check for duped inputs.
*/
class LedgerTransaction(
data class LedgerTransaction(
/** The input states which will be consumed/invalidated by the execution of this transaction. */
val inStateRefs: List<ContractStateRef>,
/** The states that will be generated by the execution of this transaction. */

View File

@ -198,6 +198,8 @@ fun createKryo(): Kryo {
register(Collections.singletonList(null).javaClass)
register(Collections.singletonMap(1, 2).javaClass)
register(ArrayList::class.java)
register(emptyList<Any>().javaClass)
register(Arrays.asList(1,3).javaClass)
// These JDK classes use a very minimal custom serialization format and are written to defend against malicious
// streams, so we can just kick it over to java serialization. We get ECPublicKeyImpl/ECPrivteKeyImpl via an
@ -223,6 +225,7 @@ fun createKryo(): Kryo {
registerDataClass<Cash.State>()
register(Cash.Commands.Move.javaClass)
registerDataClass<Cash.Commands.Exit>()
registerDataClass<Cash.Commands.Issue>()
registerDataClass<CommercialPaper.State>()
register(CommercialPaper.Commands.Move.javaClass)
register(CommercialPaper.Commands.Redeem.javaClass)

View File

@ -0,0 +1,100 @@
package core
import contracts.Cash
import core.testutils.*
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals
class TransactionGroupTests {
val A_THOUSAND_POUNDS = Cash.State(InstitutionReference(MINI_CORP, OpaqueBytes.of(1, 2, 3)), 1000.POUNDS, MINI_CORP_KEY)
@Test
fun success() {
transactionGroup {
roots {
transaction(A_THOUSAND_POUNDS label "£1000")
}
transaction {
input("£1000")
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE }
arg(MINI_CORP_KEY) { Cash.Commands.Move }
}
transaction {
input("alice's £1000")
arg(ALICE) { Cash.Commands.Move }
arg(MINI_CORP_KEY) { Cash.Commands.Exit(1000.POUNDS) }
}
verify()
}
}
@Test
fun conflict() {
transactionGroup {
val t = transaction {
output("cash") { A_THOUSAND_POUNDS }
arg(MINI_CORP_KEY) { Cash.Commands.Issue() }
}
val conflict1 = transaction {
input("cash")
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB
output { HALF }
output { HALF }
arg(MINI_CORP_KEY) { Cash.Commands.Move }
}
verify()
// Alice tries to double spend back to herself.
val conflict2 = transaction {
input("cash")
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE
output { HALF }
output { HALF }
arg(MINI_CORP_KEY) { Cash.Commands.Move }
}
assertNotEquals(conflict1, conflict2)
val e = assertFailsWith(TransactionConflictException::class) {
verify()
}
assertEquals(ContractStateRef(t.hash, 0), e.conflictRef)
assertEquals(setOf(conflict1, conflict2), setOf(e.tx1, e.tx2))
}
}
@Test
fun disconnected() {
// Check that if we have a transaction in the group that doesn't connect to anything else, it's rejected.
val tg = transactionGroup {
transaction {
output("cash") { A_THOUSAND_POUNDS }
arg(MINI_CORP_KEY) { Cash.Commands.Issue() }
}
transaction {
input("cash")
output { A_THOUSAND_POUNDS `owned by` BOB }
}
}
// We have to do this manually without the DSL because transactionGroup { } won't let us create a tx that
// points nowhere.
val ref = ContractStateRef(SecureHash.randomSHA256(), 0)
tg.txns.add(LedgerTransaction(
listOf(ref), listOf(A_THOUSAND_POUNDS), listOf(AuthenticatedObject(listOf(BOB), emptyList(), Cash.Commands.Move)), TEST_TX_TIME, SecureHash.randomSHA256())
)
val e = assertFailsWith(TransactionResolutionException::class) {
tg.verify()
}
assertEquals(e.hash, ref.txhash)
}
}

View File

@ -5,6 +5,7 @@ import core.*
import java.security.KeyPairGenerator
import java.security.PublicKey
import java.time.Instant
import java.util.*
import kotlin.test.fail
object TestUtils {
@ -17,6 +18,8 @@ val MEGA_CORP_KEY = DummyPublicKey("mini")
val MINI_CORP_KEY = DummyPublicKey("mega")
val DUMMY_PUBKEY_1 = DummyPublicKey("x1")
val DUMMY_PUBKEY_2 = DummyPublicKey("x2")
val ALICE = DummyPublicKey("alice")
val BOB = DummyPublicKey("bob")
val MEGA_CORP = Institution("MegaCorp", MEGA_CORP_KEY)
val MINI_CORP = Institution("MiniCorp", MINI_CORP_KEY)
@ -38,7 +41,7 @@ val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
// Defines a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
//
// Define a transaction like this:
//
@ -57,37 +60,41 @@ val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
//
// TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block
// Corresponds to the args to Contract.verify
class TransactionForTest() {
private val inStates = arrayListOf<ContractState>()
infix fun Cash.State.`owned by`(owner: PublicKey) = this.copy(owner = owner)
class LabeledOutput(val label: String?, val state: ContractState) {
override fun toString() = state.toString() + (if (label != null) " ($label)" else "")
override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state)
override fun hashCode(): Int = state.hashCode()
}
private val outStates = arrayListOf<LabeledOutput>()
class LabeledOutput(val label: String?, val state: ContractState) {
override fun toString() = state.toString() + (if (label != null) " ($label)" else "")
override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state)
override fun hashCode(): Int = state.hashCode()
}
private val commands: MutableList<AuthenticatedObject<Command>> = arrayListOf()
infix fun ContractState.label(label: String) = LabeledOutput(label, this)
constructor(inStates: List<ContractState>, outStates: List<ContractState>, commands: List<AuthenticatedObject<Command>>) : this() {
this.inStates.addAll(inStates)
this.outStates.addAll(outStates.map { LabeledOutput(null, it) })
this.commands.addAll(commands)
}
abstract class AbstractTransactionForTest {
protected val outStates = ArrayList<LabeledOutput>()
protected val commands = ArrayList<AuthenticatedObject<Command>>()
open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, s()).apply { outStates.add(this) }
fun input(s: () -> ContractState) = inStates.add(s())
fun output(label: String? = null, s: () -> ContractState) = outStates.add(LabeledOutput(label, s()))
fun arg(vararg key: PublicKey, c: () -> Command) {
val keys = listOf(*key)
commands.add(AuthenticatedObject(keys, keys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, c()))
}
private fun run(time: Instant) = TransactionForVerification(inStates, outStates.map { it.state }, commands, time, SecureHash.randomSHA256()).verify(TEST_PROGRAM_MAP)
// Forbid patterns like: transaction { ... transaction { ... } }
@Deprecated("Cannot nest transactions, use tweak", level = DeprecationLevel.ERROR)
fun transaction(body: TransactionForTest.() -> Unit) {}
}
infix fun `fails requirement`(msg: String) = rejects(msg)
// which is uglier?? :)
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
// Corresponds to the args to Contract.verify
open class TransactionForTest : AbstractTransactionForTest() {
private val inStates = arrayListOf<ContractState>()
fun input(s: () -> ContractState) = inStates.add(s())
protected fun run(time: Instant) {
val tx = TransactionForVerification(inStates, outStates.map { it.state }, commands, time, SecureHash.randomSHA256())
tx.verify(TEST_PROGRAM_MAP)
}
fun accepts(time: Instant = TEST_TX_TIME) = run(time)
fun rejects(withMessage: String? = null, time: Instant = TEST_TX_TIME) {
@ -105,15 +112,9 @@ class TransactionForTest() {
if (!r) throw AssertionError("Expected exception but didn't get one")
}
// Allow customisation of partial transactions.
fun tweak(body: TransactionForTest.() -> Unit): TransactionForTest {
val tx = TransactionForTest()
tx.inStates.addAll(inStates)
tx.outStates.addAll(outStates)
tx.commands.addAll(commands)
tx.body()
return tx
}
// which is uglier?? :)
infix fun `fails requirement`(msg: String) = rejects(msg)
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
// Use this to create transactions where the output of this transaction is automatically used as an input of
// the next.
@ -131,6 +132,16 @@ class TransactionForTest() {
return tx
}
// Allow customisation of partial transactions.
fun tweak(body: TransactionForTest.() -> Unit): TransactionForTest {
val tx = TransactionForTest()
tx.inStates.addAll(inStates)
tx.outStates.addAll(outStates)
tx.commands.addAll(commands)
tx.body()
return tx
}
override fun toString(): String {
return """transaction {
inputs: $inStates
@ -149,8 +160,67 @@ class TransactionForTest() {
}
}
fun transaction(body: TransactionForTest.() -> Unit): TransactionForTest {
val tx = TransactionForTest()
tx.body()
return tx
fun transaction(body: TransactionForTest.() -> Unit) = TransactionForTest().apply { body() }
class TransactionGroupForTest {
open inner class LedgerTransactionForTest : AbstractTransactionForTest() {
private val inputs = ArrayList<ContractStateRef>()
fun input(label: String) {
inputs.add(labelToRefs[label] ?: throw IllegalArgumentException("Unknown label \"$label\""))
}
fun toLedgerTransaction(time: Instant): LedgerTransaction {
val wireCmds = commands.map { WireCommand(it.value, it.signers) }
return WireTransaction(inputs, outStates.map { it.state }, wireCmds).toLedgerTransaction(time, TEST_KEYS_TO_CORP_MAP)
}
}
private inner class InternalLedgerTransactionForTest : LedgerTransactionForTest() {
fun finaliseAndInsertLabels(time: Instant): LedgerTransaction {
val ltx = toLedgerTransaction(time)
for ((index, state) in outStates.withIndex()) {
if (state.label != null)
labelToRefs[state.label] = ContractStateRef(ltx.hash, index)
}
return ltx
}
}
private val rootTxns = ArrayList<LedgerTransaction>()
private val labelToRefs = HashMap<String, ContractStateRef>()
inner class Roots {
fun transaction(vararg outputStates: LabeledOutput) {
val outs = outputStates.map { it.state }
val wtx = WireTransaction(emptyList(), outs, emptyList())
val ltx = wtx.toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP)
outputStates.forEachIndexed { index, labeledOutput -> labelToRefs[labeledOutput.label!!] = ContractStateRef(ltx.hash, index) }
rootTxns.add(ltx)
}
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
fun roots(body: Roots.() -> Unit) {}
}
fun roots(body: Roots.() -> Unit) = Roots().apply { body() }
val txns = ArrayList<LedgerTransaction>()
fun transaction(time: Instant = TEST_TX_TIME, body: LedgerTransactionForTest.() -> Unit): LedgerTransaction {
val forTest = InternalLedgerTransactionForTest()
forTest.body()
val ltx = forTest.finaliseAndInsertLabels(time)
txns.add(ltx)
return ltx
}
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
fun transactionGroup(body: TransactionGroupForTest.() -> Unit) {}
fun verify() {
toTransactionGroup().verify(TEST_PROGRAM_MAP)
}
fun toTransactionGroup() = TransactionGroup(txns.map { it }.toSet(), rootTxns.toSet())
}
fun transactionGroup(body: TransactionGroupForTest.() -> Unit) = TransactionGroupForTest().apply { this.body() }