mirror of
https://github.com/corda/corda.git
synced 2025-02-22 18:12:53 +00:00
Major: Integrate serialisation everywhere, implement basic signing/sig checking code.
Currently the serialised forms and the form fed into the contract aren't quite joined up, because on disk/network a transaction is serialised with input references i.e. (txhash, output index) pairs, but the contract wants to see all input states in fully loaded form. To do that, we need some notion of a database of transactions.
This commit is contained in:
parent
162b830bcd
commit
dad59d116c
@ -57,10 +57,7 @@ object Cash : Contract {
|
|||||||
* A command stating that money has been withdrawn from the shared ledger and is now accounted for
|
* A command stating that money has been withdrawn from the shared ledger and is now accounted for
|
||||||
* in some other way.
|
* in some other way.
|
||||||
*/
|
*/
|
||||||
class Exit(val amount: Amount) : Command {
|
data class Exit(val amount: Amount) : Command
|
||||||
override fun equals(other: Any?) = other is Exit && other.amount == amount
|
|
||||||
override fun hashCode() = amount.hashCode()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This is the function EVERYONE runs */
|
/** This is the function EVERYONE runs */
|
||||||
@ -178,7 +175,7 @@ object Cash : Contract {
|
|||||||
} else states
|
} else states
|
||||||
|
|
||||||
// Finally, generate the commands. Pretend to sign here, real signatures aren't done yet.
|
// Finally, generate the commands. Pretend to sign here, real signatures aren't done yet.
|
||||||
val commands = keysUsed.map { VerifiedSigned(listOf(it), emptyList(), Commands.Move) }
|
val commands = keysUsed.map { AuthenticatedObject(listOf(it), emptyList(), Commands.Move) }
|
||||||
|
|
||||||
return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList())
|
return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList())
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
|
import core.serialization.SerializeableWithKryo
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -8,29 +9,16 @@ import kotlin.math.div
|
|||||||
/**
|
/**
|
||||||
* Defines a simple domain specific language for the specificiation of financial contracts. Currently covers:
|
* Defines a simple domain specific language for the specificiation of financial contracts. Currently covers:
|
||||||
*
|
*
|
||||||
|
* - Some utilities for working with commands.
|
||||||
* - Code for working with currencies.
|
* - Code for working with currencies.
|
||||||
* - An Amount type that represents a positive quantity of a specific currency.
|
* - An Amount type that represents a positive quantity of a specific currency.
|
||||||
* - A simple language extension for specifying requirements in English, along with logic to enforce them.
|
* - A simple language extension for specifying requirements in English, along with logic to enforce them.
|
||||||
|
*
|
||||||
|
* TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354)
|
//// Currencies ///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// region Misc
|
|
||||||
inline fun <reified T : Command> List<VerifiedSigned<Command>>.select(signer: PublicKey? = null, institution: Institution? = null) =
|
|
||||||
filter { it.value is T }.
|
|
||||||
filter { if (signer == null) true else it.signers.contains(signer) }.
|
|
||||||
filter { if (institution == null) true else it.signingInstitutions.contains(institution) }.
|
|
||||||
map { VerifiedSigned<T>(it.signers, it.signingInstitutions, it.value as T) }
|
|
||||||
|
|
||||||
inline fun <reified T : Command> List<VerifiedSigned<Command>>.requireSingleCommand() = try {
|
|
||||||
select<T>().single()
|
|
||||||
} catch (e: NoSuchElementException) {
|
|
||||||
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Currencies
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
fun currency(code: String) = Currency.getInstance(code)
|
fun currency(code: String) = Currency.getInstance(code)
|
||||||
|
|
||||||
val USD = currency("USD")
|
val USD = currency("USD")
|
||||||
@ -40,14 +28,8 @@ val CHF = currency("CHF")
|
|||||||
val Int.DOLLARS: Amount get() = Amount(this * 100, USD)
|
val Int.DOLLARS: Amount get() = Amount(this * 100, USD)
|
||||||
val Int.POUNDS: Amount get() = Amount(this * 100, GBP)
|
val Int.POUNDS: Amount get() = Amount(this * 100, GBP)
|
||||||
val Int.SWISS_FRANCS: Amount get() = Amount(this * 100, CHF)
|
val Int.SWISS_FRANCS: Amount get() = Amount(this * 100, CHF)
|
||||||
val Double.DOLLARS: Amount get() = Amount((this * 100).toInt(), USD)
|
|
||||||
val Double.POUNDS: Amount get() = Amount((this * 100).toInt(), USD)
|
|
||||||
val Double.SWISS_FRANCS: Amount get() = Amount((this * 100).toInt(), USD)
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
|
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// region Requirements
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
class Requirements {
|
class Requirements {
|
||||||
infix fun String.by(expr: Boolean) {
|
infix fun String.by(expr: Boolean) {
|
||||||
@ -59,11 +41,7 @@ inline fun requireThat(body: Requirements.() -> Unit) {
|
|||||||
R.body()
|
R.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
//// Amounts //////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
// region Amounts
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Amount represents a positive quantity of currency, measured in pennies, which are the smallest representable units.
|
* Amount represents a positive quantity of currency, measured in pennies, which are the smallest representable units.
|
||||||
@ -75,8 +53,10 @@ inline fun requireThat(body: Requirements.() -> Unit) {
|
|||||||
*
|
*
|
||||||
* It probably makes sense to replace this with convenience extensions over the JSR 354 MonetaryAmount interface, if
|
* It probably makes sense to replace this with convenience extensions over the JSR 354 MonetaryAmount interface, if
|
||||||
* that spec doesn't turn out to be too heavy (it looks fairly complicated).
|
* that spec doesn't turn out to be too heavy (it looks fairly complicated).
|
||||||
|
*
|
||||||
|
* TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system.
|
||||||
*/
|
*/
|
||||||
data class Amount(val pennies: Int, val currency: Currency) : Comparable<Amount> {
|
data class Amount(val pennies: Int, val currency: Currency) : Comparable<Amount>, SerializeableWithKryo {
|
||||||
init {
|
init {
|
||||||
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
|
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
|
||||||
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
|
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
|
||||||
@ -113,5 +93,15 @@ fun Iterable<Amount>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThr
|
|||||||
fun Iterable<Amount>.sumOrThrow() = reduce { left, right -> left + right }
|
fun Iterable<Amount>.sumOrThrow() = reduce { left, right -> left + right }
|
||||||
fun Iterable<Amount>.sumOrZero(currency: Currency) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency)
|
fun Iterable<Amount>.sumOrZero(currency: Currency) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency)
|
||||||
|
|
||||||
// TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system.
|
//// Authenticated commands ///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// endregion
|
inline fun <reified T : Command> List<AuthenticatedObject<Command>>.select(signer: PublicKey? = null, institution: Institution? = null) =
|
||||||
|
filter { it.value is T }.
|
||||||
|
filter { if (signer == null) true else it.signers.contains(signer) }.
|
||||||
|
filter { if (institution == null) true else it.signingInstitutions.contains(institution) }.
|
||||||
|
map { AuthenticatedObject<T>(it.signers, it.signingInstitutions, it.value as T) }
|
||||||
|
|
||||||
|
inline fun <reified T : Command> List<AuthenticatedObject<Command>>.requireSingleCommand() = try {
|
||||||
|
select<T>().single()
|
||||||
|
} catch (e: NoSuchElementException) {
|
||||||
|
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
|
||||||
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding
|
import com.google.common.io.BaseEncoding
|
||||||
import java.security.MessageDigest
|
import java.security.*
|
||||||
import java.security.PublicKey
|
|
||||||
|
|
||||||
// "sealed" here means there can't be any subclasses other than the ones defined here.
|
// "sealed" here means there can't be any subclasses other than the ones defined here.
|
||||||
sealed class SecureHash(bits: ByteArray) : OpaqueBytes(bits) {
|
sealed class SecureHash(bits: ByteArray) : OpaqueBytes(bits) {
|
||||||
class SHA256(bits: ByteArray) : SecureHash(bits) {
|
class SHA256(bits: ByteArray) : SecureHash(bits) {
|
||||||
init { require(bits.size == 32) }
|
init { require(bits.size == 32) }
|
||||||
|
override val signatureAlgorithmName: String get() = "SHA256withECDSA"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Like static methods in Java, except the 'companion' is a singleton that can have state.
|
// Like static methods in Java, except the 'companion' is a singleton that can have state.
|
||||||
@ -23,17 +23,25 @@ sealed class SecureHash(bits: ByteArray) : OpaqueBytes(bits) {
|
|||||||
fun sha256(str: String) = sha256(str.toByteArray())
|
fun sha256(str: String) = sha256(str.toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract val signatureAlgorithmName: String
|
||||||
|
|
||||||
// In future, maybe SHA3, truncated hashes etc.
|
// In future, maybe SHA3, truncated hashes etc.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around a digital signature. The covering field is a generic tag usable by whatever is interpreting the
|
* A wrapper around a digital signature. The covering field is a generic tag usable by whatever is interpreting the
|
||||||
* signature.
|
* signature. It isn't used currently, but experience from Bitcoin suggests such a feature is useful, especially when
|
||||||
|
* building partially signed transactions.
|
||||||
*/
|
*/
|
||||||
sealed class DigitalSignature(bits: ByteArray, val covering: Int) : OpaqueBytes(bits) {
|
open class DigitalSignature(bits: ByteArray, val covering: Int = 0) : OpaqueBytes(bits) {
|
||||||
/** A digital signature that identifies who the public key is owned by */
|
|
||||||
open class WithKey(val by: PublicKey, bits: ByteArray, covering: Int) : DigitalSignature(bits, covering)
|
/** A digital signature that identifies who the public key is owned by. */
|
||||||
|
open class WithKey(val by: PublicKey, bits: ByteArray, covering: Int = 0) : DigitalSignature(bits, covering) {
|
||||||
|
fun verifyWithECDSA(content: ByteArray) = by.verifyWithECDSA(content, this)
|
||||||
|
}
|
||||||
|
|
||||||
class LegallyIdentifiable(val signer: Institution, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering)
|
class LegallyIdentifiable(val signer: Institution, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||||
@ -43,3 +51,21 @@ object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
|||||||
override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1
|
override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1
|
||||||
override fun toString() = "NULL_KEY"
|
override fun toString() = "NULL_KEY"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Utility to simplify the act of signing a byte array */
|
||||||
|
fun PrivateKey.signWithECDSA(bits: ByteArray, publicKey: PublicKey? = null): DigitalSignature {
|
||||||
|
val signer = Signature.getInstance("SHA256withECDSA")
|
||||||
|
signer.initSign(this)
|
||||||
|
signer.update(bits)
|
||||||
|
val sig = signer.sign()
|
||||||
|
return if (publicKey == null) DigitalSignature(sig) else DigitalSignature.WithKey(publicKey, sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Utility to simplify the act of verifying a signature */
|
||||||
|
fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) {
|
||||||
|
val verifier = Signature.getInstance("SHA256withECDSA")
|
||||||
|
verifier.initVerify(this)
|
||||||
|
verifier.update(content)
|
||||||
|
if (verifier.verify(signature.bits) == false)
|
||||||
|
throw SignatureException("Signature did not match")
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
|
import core.serialization.SerializeableWithKryo
|
||||||
|
import core.serialization.serialize
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -7,7 +9,7 @@ import java.security.PublicKey
|
|||||||
* file that the program can use to persist data across transactions. States are immutable: once created they are never
|
* file that the program can use to persist data across transactions. States are immutable: once created they are never
|
||||||
* updated, instead, any changes must generate a new successor state.
|
* updated, instead, any changes must generate a new successor state.
|
||||||
*/
|
*/
|
||||||
interface ContractState {
|
interface ContractState : SerializeableWithKryo {
|
||||||
/**
|
/**
|
||||||
* Refers to a bytecode program that has previously been published to the network. This contract program
|
* Refers to a bytecode program that has previously been published to the network. This contract program
|
||||||
* will be executed any time this state is used in an input. It must accept in order for the
|
* will be executed any time this state is used in an input. It must accept in order for the
|
||||||
@ -16,34 +18,30 @@ interface ContractState {
|
|||||||
val programRef: SecureHash
|
val programRef: SecureHash
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */
|
||||||
* A stateref is a pointer to a state, this is an equivalent of an "outpoint" in Bitcoin.
|
fun ContractState.hash(): SecureHash = SecureHash.sha256((serialize()))
|
||||||
*/
|
|
||||||
class ContractStateRef(private val txhash: SecureHash.SHA256, private val index: Int)
|
|
||||||
|
|
||||||
class Institution(
|
/** A stateref is a pointer to a state, this is an equivalent of an "outpoint" in Bitcoin. */
|
||||||
val name: String,
|
data class ContractStateRef(val txhash: SecureHash.SHA256, val index: Int) : SerializeableWithKryo
|
||||||
val owningKey: PublicKey
|
|
||||||
) {
|
/** An Institution is well known (name, pubkey) pair. In a real system this would probably be an X.509 certificate. */
|
||||||
|
data class Institution(val name: String, val owningKey: PublicKey) : SerializeableWithKryo {
|
||||||
override fun toString() = name
|
override fun toString() = name
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Marker interface for objects that represent commands */
|
/**
|
||||||
interface Command
|
* Reference to something being stored or issued by an institution e.g. in a vault or (more likely) on their normal
|
||||||
|
* ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the institution.
|
||||||
|
*/
|
||||||
|
data class InstitutionReference(val institution: Institution, val reference: OpaqueBytes) : SerializeableWithKryo {
|
||||||
|
override fun toString() = "${institution.name}$reference"
|
||||||
|
}
|
||||||
|
|
||||||
/** Provided as an input to a contract; converted to a [VerifiedSigned] by the platform before execution. */
|
/** Marker interface for classes that represent commands */
|
||||||
data class SignedCommand(
|
interface Command : SerializeableWithKryo
|
||||||
/** Signatures over this object to prove who it came from: this is fetched off the end of the transaction wire format. */
|
|
||||||
val commandDataSignatures: List<DigitalSignature.WithKey>,
|
|
||||||
|
|
||||||
/** Command data, deserialized to an implementation of [Command] */
|
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
|
||||||
val serialized: OpaqueBytes,
|
data class AuthenticatedObject<out T : Any>(
|
||||||
/** Identifies what command the serialized data contains (hash of bytecode?) */
|
|
||||||
val classID: SecureHash
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Obtained from a [SignedCommand], deserialised and signature checked */
|
|
||||||
data class VerifiedSigned<out T : Command>(
|
|
||||||
val signers: List<PublicKey>,
|
val signers: List<PublicKey>,
|
||||||
/** If any public keys were recognised, the looked up institutions are available here */
|
/** If any public keys were recognised, the looked up institutions are available here */
|
||||||
val signingInstitutions: List<Institution>,
|
val signingInstitutions: List<Institution>,
|
||||||
@ -69,11 +67,3 @@ interface Contract {
|
|||||||
/** Unparsed reference to the natural language contract that this code is supposed to express (usually a URL). */
|
/** Unparsed reference to the natural language contract that this code is supposed to express (usually a URL). */
|
||||||
val legalContractReference: String
|
val legalContractReference: String
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reference to something being stored or issued by an institution e.g. in a vault or (more likely) on their normal
|
|
||||||
* ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the institution.
|
|
||||||
*/
|
|
||||||
data class InstitutionReference(val institution: Institution, val reference: OpaqueBytes) {
|
|
||||||
override fun toString() = "${institution.name}$reference"
|
|
||||||
}
|
|
@ -1,12 +1,13 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import contracts.*
|
import contracts.*
|
||||||
|
import core.serialization.SerializeableWithKryo
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import kotlin.test.fail
|
import kotlin.test.fail
|
||||||
|
|
||||||
class DummyPublicKey(private val s: String) : PublicKey, Comparable<PublicKey> {
|
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey>, SerializeableWithKryo {
|
||||||
override fun getAlgorithm() = "DUMMY"
|
override fun getAlgorithm() = "DUMMY"
|
||||||
override fun getEncoded() = s.toByteArray()
|
override fun getEncoded() = s.toByteArray()
|
||||||
override fun getFormat() = "ASN.1"
|
override fun getFormat() = "ASN.1"
|
||||||
@ -69,9 +70,9 @@ class TransactionForTest() {
|
|||||||
override fun hashCode(): Int = state.hashCode()
|
override fun hashCode(): Int = state.hashCode()
|
||||||
}
|
}
|
||||||
private val outStates = arrayListOf<LabeledOutput>()
|
private val outStates = arrayListOf<LabeledOutput>()
|
||||||
private val args: MutableList<VerifiedSigned<Command>> = arrayListOf()
|
private val args: MutableList<AuthenticatedObject<Command>> = arrayListOf()
|
||||||
|
|
||||||
constructor(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>) : this() {
|
constructor(inStates: List<ContractState>, outStates: List<ContractState>, args: List<AuthenticatedObject<Command>>) : this() {
|
||||||
this.inStates.addAll(inStates)
|
this.inStates.addAll(inStates)
|
||||||
this.outStates.addAll(outStates.map { LabeledOutput(null, it) })
|
this.outStates.addAll(outStates.map { LabeledOutput(null, it) })
|
||||||
this.args.addAll(args)
|
this.args.addAll(args)
|
||||||
@ -82,7 +83,7 @@ class TransactionForTest() {
|
|||||||
fun arg(vararg key: PublicKey, c: () -> Command) {
|
fun arg(vararg key: PublicKey, c: () -> Command) {
|
||||||
val keys = listOf(*key)
|
val keys = listOf(*key)
|
||||||
// TODO: replace map->filterNotNull once upgraded to next Kotlin
|
// TODO: replace map->filterNotNull once upgraded to next Kotlin
|
||||||
args.add(VerifiedSigned(keys, keys.map { TEST_KEYS_TO_CORP_MAP[it] }.filterNotNull(), c()))
|
args.add(AuthenticatedObject(keys, keys.map { TEST_KEYS_TO_CORP_MAP[it] }.filterNotNull(), c()))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun run(time: Instant) = TransactionForVerification(inStates, outStates.map { it.state }, args, time).verify(TEST_PROGRAM_MAP)
|
private fun run(time: Instant) = TransactionForVerification(inStates, outStates.map { it.state }, args, time).verify(TEST_PROGRAM_MAP)
|
||||||
|
@ -1,33 +1,124 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
|
import core.serialization.SerializeableWithKryo
|
||||||
|
import core.serialization.deserialize
|
||||||
|
import core.serialization.serialize
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.security.SignatureException
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
// Various views of transactions as they progress through the pipeline:
|
/**
|
||||||
//
|
* Views of a transaction as it progresses through the pipeline, from bytes loaded from disk/network to the object
|
||||||
// TimestampedWireTransaction(WireTransaction) -> LedgerTransaction -> TransactionForVerification
|
* tree passed into a contract.
|
||||||
// TransactionForTest
|
*
|
||||||
|
* TimestampedWireTransaction wraps a serialized SignedWireTransaction. The timestamp is a signature from a timestamping
|
||||||
|
* authority and is what gives the contract a sense of time. This isn't used yet.
|
||||||
|
*
|
||||||
|
* SignedWireTransaction wraps a serialized WireTransaction. It contains one or more ECDSA signatures, each one from
|
||||||
|
* a public key that is mentioned inside a transaction command.
|
||||||
|
*
|
||||||
|
* WireTransaction is a transaction in a form ready to be serialised/unserialised/hashed. This is the object from which
|
||||||
|
* a transaction ID (hash) is calculated. It contains no signatures and no timestamp. That means, sending a transaction
|
||||||
|
* to a timestamping authority does NOT change its hash (this may be an issue that leads to confusion and should be
|
||||||
|
* examined more closely).
|
||||||
|
*
|
||||||
|
* LedgerTransaction is derived from WireTransaction and TimestampedWireTransaction together. It is the result of
|
||||||
|
* doing some basic key lookups on WireCommand to see if any keys are from a recognised institution, thus converting
|
||||||
|
* the WireCommand objects into AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->institution
|
||||||
|
* map. In future it'd make more sense to use a certificate scheme and so that logic would get more complex.
|
||||||
|
*
|
||||||
|
* All the above refer to inputs using a (txhash, output index) pair.
|
||||||
|
*
|
||||||
|
* TransactionForVerification is the same as LedgerTransaction but with the input states looked up from a local
|
||||||
|
* database and replaced with the real objects. TFV is the form that is finally fed into the contracts.
|
||||||
|
*/
|
||||||
|
|
||||||
class WireTransaction(
|
/** Serialized command plus pubkey pair: the signature is stored at the end of the serialized bytes */
|
||||||
// TODO: This is supposed to be a protocol buffer, FIX SPE message, etc. For prototype it can just be Kryo serialised.
|
data class WireCommand(val command: Command, val pubkeys: List<PublicKey>) : SerializeableWithKryo
|
||||||
val tx: ByteArray,
|
|
||||||
|
|
||||||
// We assume Ed25519 signatures for all. Num signatures == array.length / 64 (each sig is 64 bytes in size)
|
/** Transaction ready for serialisation, without any signatures attached. */
|
||||||
// This array is in the same order as the public keys in the commands array, so signatures can be matched to
|
data class WireTransaction(val inputStates: List<ContractStateRef>,
|
||||||
// public keys in that manner.
|
val outputStates: List<ContractState>,
|
||||||
val signatures: ByteArray
|
val args: List<WireCommand>) : SerializeableWithKryo {
|
||||||
)
|
fun signWith(keys: List<KeyPair>): SignedWireTransaction {
|
||||||
|
val keyMap = keys.map { it.public to it.private }.toMap()
|
||||||
|
val bits = serialize()
|
||||||
|
val signWith = args.flatMap { it.pubkeys }.toSet()
|
||||||
|
if (keys.size != signWith.size)
|
||||||
|
throw IllegalArgumentException("Incorrect number of keys provided: ${keys.size} vs ${signWith.size}")
|
||||||
|
val sigs = ArrayList<DigitalSignature.WithKey>()
|
||||||
|
for (key in signWith) {
|
||||||
|
val priv = keyMap[key] ?: throw IllegalArgumentException("Command without private signing key found")
|
||||||
|
sigs.add(priv.signWithECDSA(bits, key) as DigitalSignature.WithKey)
|
||||||
|
}
|
||||||
|
return SignedWireTransaction(bits, sigs)
|
||||||
|
}
|
||||||
|
|
||||||
class TimestampedWireTransaction(
|
val hash: SecureHash get() = SecureHash.sha256(serialize())
|
||||||
// A serialised WireTransaction
|
|
||||||
|
fun toLedgerTransaction(timestamp: Instant, institutionKeyMap: Map<PublicKey, Institution>): LedgerTransaction {
|
||||||
|
val authenticatedArgs = args.map {
|
||||||
|
// TODO: Replace map/filterNotNull with mapNotNull on next Kotlin upgrade.
|
||||||
|
val institutions = it.pubkeys.map { pk -> institutionKeyMap[pk] }.filterNotNull()
|
||||||
|
AuthenticatedObject(it.pubkeys, institutions, it.command)
|
||||||
|
}
|
||||||
|
return LedgerTransaction(inputStates, outputStates, authenticatedArgs, timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SignedWireTransaction(val txBits: ByteArray, val sigs: List<DigitalSignature.WithKey>) : SerializeableWithKryo {
|
||||||
|
init {
|
||||||
|
check(sigs.isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the given signatures against the serialized transaction data. Does NOT deserialise or check the contents
|
||||||
|
* to ensure there are no missing signatures: use verify() to do that. This weaker version can be useful for
|
||||||
|
* checking a partially signed transaction being prepared by multiple co-operating parties.
|
||||||
|
*
|
||||||
|
* @throws SignatureException if the signature is invalid or does not match.
|
||||||
|
*/
|
||||||
|
fun verifySignatures() {
|
||||||
|
for (sig in sigs)
|
||||||
|
sig.verifyWithECDSA(txBits)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the signatures, deserialise the wire transaction and then check that the set of signatures found matches
|
||||||
|
* the set of pubkeys in the commands.
|
||||||
|
*
|
||||||
|
* @throws SignatureException if the signature is invalid or does not match.
|
||||||
|
*/
|
||||||
|
fun verify(): WireTransaction {
|
||||||
|
verifySignatures()
|
||||||
|
val wtx = txBits.deserialize<WireTransaction>()
|
||||||
|
// Verify that every command key was in the set that we just verified: there should be no commands that were
|
||||||
|
// unverified.
|
||||||
|
val cmdKeys = wtx.args.flatMap { it.pubkeys }.toSet()
|
||||||
|
val sigKeys = sigs.map { it.by }.toSet()
|
||||||
|
if (cmdKeys != sigKeys)
|
||||||
|
throw SignatureException("Command keys don't match the signatures: $cmdKeys vs $sigKeys")
|
||||||
|
return wtx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not used yet.
|
||||||
|
data class TimestampedWireTransaction(
|
||||||
|
/** A serialised SignedWireTransaction */
|
||||||
val wireTX: ByteArray,
|
val wireTX: ByteArray,
|
||||||
|
|
||||||
// This is, for example, an RFC 3161 serialised structure (but we probably want something more compact).
|
/** Signature from a timestamping authority. For instance using RFC 3161 */
|
||||||
val timestamp: ByteArray
|
val timestamp: ByteArray
|
||||||
)
|
) : SerializeableWithKryo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A LedgerTransaction wraps the data needed to calculate one or more successor states from a set of input states.
|
* A LedgerTransaction wraps the data needed to calculate one or more successor states from a set of input states.
|
||||||
* It is the first step after extraction from a WireTransaction. The signature part is tricky.
|
* It is the first step after extraction from a WireTransaction. The signatures at this point have been lined up
|
||||||
|
* with the commands from the wire, and verified/looked up.
|
||||||
|
*
|
||||||
|
* Not used yet.
|
||||||
*/
|
*/
|
||||||
class LedgerTransaction(
|
class LedgerTransaction(
|
||||||
/** The input states which will be consumed/invalidated by the execution of this transaction. */
|
/** The input states which will be consumed/invalidated by the execution of this transaction. */
|
||||||
@ -35,20 +126,18 @@ class LedgerTransaction(
|
|||||||
/** The states that will be generated by the execution of this transaction. */
|
/** The states that will be generated by the execution of this transaction. */
|
||||||
val outputStates: List<ContractState>,
|
val outputStates: List<ContractState>,
|
||||||
/** Arbitrary data passed to the program of each input state. */
|
/** Arbitrary data passed to the program of each input state. */
|
||||||
val args: List<SignedCommand>,
|
val args: List<AuthenticatedObject<Command>>,
|
||||||
/** The moment the transaction was timestamped for */
|
/** The moment the transaction was timestamped for */
|
||||||
val time: Instant
|
val time: Instant
|
||||||
|
|
||||||
// TODO: nLockTime equivalent?
|
// TODO: nLockTime equivalent?
|
||||||
)
|
)
|
||||||
|
|
||||||
/** A transaction in fully resolved form, ready for passing as input to a verification function */
|
/** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */
|
||||||
class TransactionForVerification(
|
class TransactionForVerification(val inStates: List<ContractState>,
|
||||||
val inStates: List<ContractState>,
|
|
||||||
val outStates: List<ContractState>,
|
val outStates: List<ContractState>,
|
||||||
val args: List<VerifiedSigned<Command>>,
|
val args: List<AuthenticatedObject<Command>>,
|
||||||
val time: Instant
|
val time: Instant) {
|
||||||
) {
|
|
||||||
fun verify(programMap: Map<SecureHash, Contract>) {
|
fun verify(programMap: Map<SecureHash, Contract>) {
|
||||||
// For each input and output state, locate the program to run. Then execute the verification function. If any
|
// For each input and output state, locate the program to run. Then execute the verification function. If any
|
||||||
// throws an exception, the entire transaction is invalid.
|
// throws an exception, the entire transaction is invalid.
|
||||||
@ -58,4 +147,5 @@ class TransactionForVerification(
|
|||||||
program.verify(this)
|
program.verify(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,11 +1,12 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding
|
import com.google.common.io.BaseEncoding
|
||||||
|
import core.serialization.SerializeableWithKryo
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/** A simple class that wraps a byte array and makes the equals/hashCode/toString methods work as you actually expect */
|
/** A simple class that wraps a byte array and makes the equals/hashCode/toString methods work as you actually expect */
|
||||||
open class OpaqueBytes(val bits: ByteArray) {
|
open class OpaqueBytes(val bits: ByteArray) : SerializeableWithKryo {
|
||||||
companion object {
|
companion object {
|
||||||
fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b))
|
fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b))
|
||||||
}
|
}
|
||||||
@ -17,7 +18,6 @@ open class OpaqueBytes(val bits: ByteArray) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode() = Arrays.hashCode(bits)
|
override fun hashCode() = Arrays.hashCode(bits)
|
||||||
|
|
||||||
override fun toString() = "[" + BaseEncoding.base16().encode(bits) + "]"
|
override fun toString() = "[" + BaseEncoding.base16().encode(bits) + "]"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,13 +6,14 @@ import com.esotericsoftware.kryo.Serializer
|
|||||||
import com.esotericsoftware.kryo.io.Input
|
import com.esotericsoftware.kryo.io.Input
|
||||||
import com.esotericsoftware.kryo.io.Output
|
import com.esotericsoftware.kryo.io.Output
|
||||||
import com.esotericsoftware.kryo.serializers.JavaSerializer
|
import com.esotericsoftware.kryo.serializers.JavaSerializer
|
||||||
import core.Amount
|
import contracts.Cash
|
||||||
import core.InstitutionReference
|
import contracts.ComedyPaper
|
||||||
import core.OpaqueBytes
|
import core.*
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.security.PublicKey
|
import java.security.KeyPairGenerator
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.reflect.KMutableProperty
|
import kotlin.reflect.KMutableProperty
|
||||||
import kotlin.reflect.jvm.javaType
|
import kotlin.reflect.jvm.javaType
|
||||||
@ -68,9 +69,10 @@ import kotlin.reflect.primaryConstructor
|
|||||||
*
|
*
|
||||||
* - Must be immutable: all properties are final. Note that Kotlin never generates bare public fields, but we should
|
* - Must be immutable: all properties are final. Note that Kotlin never generates bare public fields, but we should
|
||||||
* add some checks for this being done anyway for cases where a contract is defined using Java.
|
* add some checks for this being done anyway for cases where a contract is defined using Java.
|
||||||
* - The only properties that exist must be arguments to the constructor. This will need to be relaxed to allow
|
|
||||||
* for pure-code (no backing state) getters, and to support constant fields like legalContractRef.
|
|
||||||
* - Requires that the data class be marked as intended for serialization using a marker interface.
|
* - Requires that the data class be marked as intended for serialization using a marker interface.
|
||||||
|
* - Properties that are not in the constructor are not serialised (but as they are final, they must be either
|
||||||
|
* initialised to a constant or derived from the constructor arguments unless they are reading external state,
|
||||||
|
* which is intended to be forbidden).
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* CONVENIENCE
|
* CONVENIENCE
|
||||||
@ -96,6 +98,11 @@ import kotlin.reflect.primaryConstructor
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marker interface for classes to use with [DataClassSerializer]. Note that only constructor defined properties will
|
||||||
|
* be serialised!
|
||||||
|
*/
|
||||||
interface SerializeableWithKryo
|
interface SerializeableWithKryo
|
||||||
|
|
||||||
class DataClassSerializer<T : SerializeableWithKryo>(val klass: KClass<T>) : Serializer<T>() {
|
class DataClassSerializer<T : SerializeableWithKryo>(val klass: KClass<T>) : Serializer<T>() {
|
||||||
@ -104,12 +111,7 @@ class DataClassSerializer<T : SerializeableWithKryo>(val klass: KClass<T>) : Ser
|
|||||||
val constructor = klass.primaryConstructor!!
|
val constructor = klass.primaryConstructor!!
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Verify that this class is safe to serialise.
|
// Verify that this class is immutable (all properties are final)
|
||||||
//
|
|
||||||
// 1) No properties that aren't in the constructor.
|
|
||||||
// 2) Objects are immutable (all properties are final)
|
|
||||||
assert(props.size == constructor.parameters.size)
|
|
||||||
assert(props.map { it.name }.toSortedSet() == constructor.parameters.map { it.name }.toSortedSet())
|
|
||||||
assert(props.none { it is KMutableProperty<*> })
|
assert(props.none { it is KMutableProperty<*> })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +128,11 @@ class DataClassSerializer<T : SerializeableWithKryo>(val klass: KClass<T>) : Ser
|
|||||||
"byte" -> output.writeByte(kProperty.get(obj) as Byte)
|
"byte" -> output.writeByte(kProperty.get(obj) as Byte)
|
||||||
"double" -> output.writeDouble(kProperty.get(obj) as Double)
|
"double" -> output.writeDouble(kProperty.get(obj) as Double)
|
||||||
"float" -> output.writeFloat(kProperty.get(obj) as Float)
|
"float" -> output.writeFloat(kProperty.get(obj) as Float)
|
||||||
else -> kryo.writeClassAndObject(output, kProperty.get(obj))
|
else -> try {
|
||||||
|
kryo.writeClassAndObject(output, kProperty.get(obj))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("Failed to serialize ${param.name} in ${klass.qualifiedName}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,10 +169,12 @@ class DataClassSerializer<T : SerializeableWithKryo>(val klass: KClass<T>) : Ser
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : SerializeableWithKryo> Kryo.registerDataClass() = register(T::class.java, DataClassSerializer(T::class))
|
val THREAD_LOCAL_KRYO = ThreadLocal.withInitial { createKryo() }
|
||||||
inline fun <reified T : SerializeableWithKryo> ByteArray.deserialize(kryo: Kryo): T = kryo.readObject(Input(this), T::class.java)
|
|
||||||
|
|
||||||
fun SerializeableWithKryo.serialize(kryo: Kryo): ByteArray {
|
inline fun <reified T : SerializeableWithKryo> Kryo.registerDataClass() = register(T::class.java, DataClassSerializer(T::class))
|
||||||
|
inline fun <reified T : SerializeableWithKryo> ByteArray.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T = kryo.readObject(Input(this), T::class.java)
|
||||||
|
|
||||||
|
fun SerializeableWithKryo.serialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): ByteArray {
|
||||||
val stream = ByteArrayOutputStream()
|
val stream = ByteArrayOutputStream()
|
||||||
Output(stream).use {
|
Output(stream).use {
|
||||||
kryo.writeObject(it, this)
|
kryo.writeObject(it, this)
|
||||||
@ -174,20 +182,52 @@ fun SerializeableWithKryo.serialize(kryo: Kryo): ByteArray {
|
|||||||
return stream.toByteArray()
|
return stream.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun kryo(): Kryo {
|
private val UNUSED_EC_KEYPAIR = KeyPairGenerator.getInstance("EC").genKeyPair()
|
||||||
|
|
||||||
|
fun createKryo(): Kryo {
|
||||||
return Kryo().apply {
|
return Kryo().apply {
|
||||||
|
// Require explicit listing of all types that can be deserialised, to defend against classes that aren't
|
||||||
|
// designed for serialisation being unexpectedly instantiated.
|
||||||
isRegistrationRequired = true
|
isRegistrationRequired = true
|
||||||
|
|
||||||
|
// Allow various array and list types. Sometimes when the type is private/internal we have to give an example
|
||||||
|
// instead and then get the class from that. These have built in Kryo serializers that are safe to use.
|
||||||
register(ByteArray::class.java)
|
register(ByteArray::class.java)
|
||||||
register(IntArray::class.java)
|
register(Collections.EMPTY_LIST.javaClass)
|
||||||
|
register(Collections.EMPTY_MAP.javaClass)
|
||||||
|
register(Collections.singletonList(null).javaClass)
|
||||||
|
register(Collections.singletonMap(1, 2).javaClass)
|
||||||
|
register(ArrayList::class.java)
|
||||||
|
|
||||||
// These JDK classes use a very minimal custom serialization format and are written to defend against malicious
|
// 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.
|
// streams, so we can just kick it over to java serialization. We get ECPublicKeyImpl/ECPrivteKeyImpl via an
|
||||||
|
// example: it'd be faster to just import the sun.security.ec package directly, but that wouldn't play nice
|
||||||
|
// when Java 9 is released, as Project Jigsaw will make internal packages will become unavailable without hacks.
|
||||||
register(Instant::class.java, JavaSerializer())
|
register(Instant::class.java, JavaSerializer())
|
||||||
register(PublicKey::class.java, JavaSerializer())
|
register(Currency::class.java, JavaSerializer()) // Only serialises the currency code as a string.
|
||||||
|
register(UNUSED_EC_KEYPAIR.private.javaClass, JavaSerializer())
|
||||||
|
register(UNUSED_EC_KEYPAIR.public.javaClass, JavaSerializer())
|
||||||
|
|
||||||
// Now register platform types.
|
// Now register platform types.
|
||||||
|
registerDataClass<SecureHash.SHA256>()
|
||||||
registerDataClass<Amount>()
|
registerDataClass<Amount>()
|
||||||
registerDataClass<InstitutionReference>()
|
registerDataClass<InstitutionReference>()
|
||||||
|
registerDataClass<Institution>()
|
||||||
registerDataClass<OpaqueBytes>()
|
registerDataClass<OpaqueBytes>()
|
||||||
|
registerDataClass<SignedWireTransaction>()
|
||||||
|
registerDataClass<ContractStateRef>()
|
||||||
|
registerDataClass<WireTransaction>()
|
||||||
|
registerDataClass<WireCommand>()
|
||||||
|
|
||||||
|
// TODO: This is obviously a short term hack: there needs to be a way to bundle up and register contracts.
|
||||||
|
registerDataClass<Cash.State>()
|
||||||
|
register(Cash.Commands.Move.javaClass)
|
||||||
|
registerDataClass<Cash.Commands.Exit>()
|
||||||
|
registerDataClass<ComedyPaper.State>()
|
||||||
|
register(ComedyPaper.Commands.Move.javaClass)
|
||||||
|
register(ComedyPaper.Commands.Redeem.javaClass)
|
||||||
|
|
||||||
|
// And for unit testing ...
|
||||||
|
registerDataClass<DummyPublicKey>()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -34,7 +34,7 @@ class CashTests {
|
|||||||
transaction {
|
transaction {
|
||||||
output { outState }
|
output { outState }
|
||||||
// No command arguments
|
// No command arguments
|
||||||
this `fails requirement` "required move command"
|
this `fails requirement` "required contracts.Cash.Commands.Move command"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
output { outState }
|
output { outState }
|
||||||
@ -167,7 +167,7 @@ class CashTests {
|
|||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
|
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
|
||||||
this `fails requirement` "required move command"
|
this `fails requirement` "required contracts.Cash.Commands.Move command"
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move }
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move }
|
||||||
|
@ -8,16 +8,15 @@ import kotlin.test.assertEquals
|
|||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
import kotlin.test.assertNull
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
data class Person(val name: String, val birthday: Instant?) : SerializeableWithKryo
|
class KryoTests {
|
||||||
|
data class Person(val name: String, val birthday: Instant?) : SerializeableWithKryo
|
||||||
data class MustBeWhizzy(val s: String) : SerializeableWithKryo {
|
data class MustBeWhizzy(val s: String) : SerializeableWithKryo {
|
||||||
init {
|
init {
|
||||||
assert(s.startsWith("whiz")) { "must be whizzy" }
|
assert(s.startsWith("whiz")) { "must be whizzy" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class KryoTests {
|
private val kryo: Kryo = createKryo().apply {
|
||||||
private val kryo: Kryo = kryo().apply {
|
|
||||||
registerDataClass<Person>()
|
registerDataClass<Person>()
|
||||||
registerDataClass<MustBeWhizzy>()
|
registerDataClass<MustBeWhizzy>()
|
||||||
}
|
}
|
||||||
|
56
tests/serialization/TransactionSerializationTests.kt
Normal file
56
tests/serialization/TransactionSerializationTests.kt
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package serialization
|
||||||
|
|
||||||
|
import contracts.Cash
|
||||||
|
import core.*
|
||||||
|
import org.junit.Test
|
||||||
|
import testutils.TestUtils
|
||||||
|
import java.security.SignatureException
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class TransactionSerializationTests {
|
||||||
|
// Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change).
|
||||||
|
// It refers to a fake TX/state that we don't bother creating here.
|
||||||
|
val depositRef = InstitutionReference(MINI_CORP, OpaqueBytes.of(1))
|
||||||
|
val outputState = Cash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1)
|
||||||
|
val changeState = Cash.State(depositRef, 400.POUNDS, TestUtils.keypair.public)
|
||||||
|
|
||||||
|
val fakeStateRef = ContractStateRef(SecureHash.sha256("fake tx id"), 0)
|
||||||
|
val tx = WireTransaction(
|
||||||
|
arrayListOf(fakeStateRef),
|
||||||
|
arrayListOf(outputState, changeState),
|
||||||
|
arrayListOf(WireCommand(Cash.Commands.Move, arrayListOf(TestUtils.keypair.public)))
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun signWireTX() {
|
||||||
|
val signedTX: SignedWireTransaction = tx.signWith(listOf(TestUtils.keypair))
|
||||||
|
|
||||||
|
// Now check that the signature we just made verifies.
|
||||||
|
signedTX.verify()
|
||||||
|
|
||||||
|
// Corrupt the data and ensure the signature catches the problem.
|
||||||
|
signedTX.txBits[5] = 0
|
||||||
|
assertFailsWith(SignatureException::class) {
|
||||||
|
signedTX.verify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun wrongKeys() {
|
||||||
|
// Can't sign without the private key.
|
||||||
|
assertFailsWith(IllegalArgumentException::class) {
|
||||||
|
tx.signWith(listOf())
|
||||||
|
}
|
||||||
|
// Can't sign with too many keys.
|
||||||
|
assertFailsWith(IllegalArgumentException::class) {
|
||||||
|
tx.signWith(listOf(TestUtils.keypair, TestUtils.keypair2))
|
||||||
|
}
|
||||||
|
|
||||||
|
val signedTX = tx.signWith(listOf(TestUtils.keypair))
|
||||||
|
|
||||||
|
// If the signature was removed in transit, we don't like it.
|
||||||
|
assertFailsWith(SignatureException::class) {
|
||||||
|
signedTX.copy(sigs = emptyList()).verify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
tests/testutils/TestUtils.kt
Normal file
8
tests/testutils/TestUtils.kt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package testutils
|
||||||
|
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
|
||||||
|
object TestUtils {
|
||||||
|
val keypair = KeyPairGenerator.getInstance("EC").genKeyPair()
|
||||||
|
val keypair2 = KeyPairGenerator.getInstance("EC").genKeyPair()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user