mirror of
https://github.com/corda/corda.git
synced 2025-02-20 09:26:41 +00:00
Reorganise how time is handled: timestamping authorities are now oracles who sign the entire transaction.
As a result, TimestampedWireTransaction is gone. Timestamp fields are gone. Timestamp commands contain before/after fields. The notion of time tolerance is now a part of the timestamping interface and timestamp data. TODO: - Unit tests to verify the notBefore/notAfter logic - Documentation update
This commit is contained in:
parent
6144ccc2c7
commit
784452ac50
@ -68,7 +68,7 @@ class CommercialPaper : Contract {
|
||||
// There are two possible things that can be done with this CP. The first is trading it. The second is redeeming
|
||||
// it for cash on or after the maturity date.
|
||||
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
|
||||
val time = tx.time
|
||||
val timestamp: TimestampCommand? = tx.getTimestampBy(DummyTimestampingAuthority.identity)
|
||||
|
||||
for (group in groups) {
|
||||
when (command.value) {
|
||||
@ -83,7 +83,7 @@ class CommercialPaper : Contract {
|
||||
is Commands.Redeem -> {
|
||||
val input = group.inputs.single()
|
||||
val received = tx.outStates.sumCashBy(input.owner)
|
||||
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
|
||||
val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped")
|
||||
requireThat {
|
||||
"the paper must have matured" by (time > input.maturityDate)
|
||||
"the received amount equals the face value" by (received == input.faceValue)
|
||||
@ -94,7 +94,7 @@ class CommercialPaper : Contract {
|
||||
|
||||
is Commands.Issue -> {
|
||||
val output = group.outputs.single()
|
||||
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
|
||||
val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
|
||||
requireThat {
|
||||
// Don't allow people to issue commercial paper under other entities identities.
|
||||
"the issuance is signed by the claimed issuer of the paper" by
|
||||
|
@ -79,7 +79,9 @@ class CrowdFund : Contract {
|
||||
val command = tx.commands.requireSingleCommand<CrowdFund.Commands>()
|
||||
val outputCrowdFund: CrowdFund.State = tx.outStates.filterIsInstance<CrowdFund.State>().single()
|
||||
val outputCash: List<Cash.State> = tx.outStates.filterIsInstance<Cash.State>()
|
||||
val time = tx.time
|
||||
|
||||
val time = tx.getTimestampBy(DummyTimestampingAuthority.identity)?.midpoint
|
||||
if (time == null) throw IllegalArgumentException("must be timestamped")
|
||||
|
||||
when (command.value) {
|
||||
is Commands.Register -> {
|
||||
@ -89,7 +91,7 @@ class CrowdFund : Contract {
|
||||
"the output registration is empty of pledges" by (outputCrowdFund.pledges.isEmpty())
|
||||
"the output registration has a non-zero target" by (outputCrowdFund.campaign.target.pennies > 0)
|
||||
"the output registration has a name" by (outputCrowdFund.campaign.name.isNotBlank())
|
||||
"the output registration has a closing time in the future" by (outputCrowdFund.campaign.closingTime > tx.time)
|
||||
"the output registration has a closing time in the future" by (time < outputCrowdFund.campaign.closingTime)
|
||||
"the output registration has an open state" by (!outputCrowdFund.closed)
|
||||
}
|
||||
}
|
||||
@ -97,7 +99,6 @@ class CrowdFund : Contract {
|
||||
is Commands.Pledge -> {
|
||||
val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single()
|
||||
val pledgedCash = outputCash.sumCashBy(inputCrowdFund.campaign.owner)
|
||||
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
|
||||
requireThat {
|
||||
"campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign)
|
||||
"the campaign is still open" by (inputCrowdFund.campaign.closingTime >= time)
|
||||
@ -117,8 +118,6 @@ class CrowdFund : Contract {
|
||||
return true
|
||||
}
|
||||
|
||||
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
|
||||
|
||||
requireThat {
|
||||
"campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign)
|
||||
"the closing date has past" by (time >= outputCrowdFund.campaign.closingTime)
|
||||
|
@ -116,7 +116,10 @@ public class JavaCommercialPaper implements Contract {
|
||||
// Find the command that instructs us what to do and check there's exactly one.
|
||||
AuthenticatedObject<CommandData> cmd = requireSingleCommand(tx.getCommands(), Commands.class);
|
||||
|
||||
Instant time = tx.getTime(); // Can be null/missing.
|
||||
TimestampCommand timestampCommand = tx.getTimestampBy(DummyTimestampingAuthority.INSTANCE.getIdentity());
|
||||
if (timestampCommand == null)
|
||||
throw new IllegalArgumentException("must be timestamped");
|
||||
Instant time = timestampCommand.getMidpoint();
|
||||
|
||||
for (InOutGroup<State> group : groups) {
|
||||
List<State> inputs = group.getInputs();
|
||||
@ -138,8 +141,6 @@ public class JavaCommercialPaper implements Contract {
|
||||
throw new IllegalStateException("Failed requirement: the output state is the same as the input state except for owner");
|
||||
} else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) {
|
||||
Amount received = CashKt.sumCashOrNull(inputs);
|
||||
if (time == null)
|
||||
throw new IllegalArgumentException("Redemption transactions must be timestamped");
|
||||
if (received == null)
|
||||
throw new IllegalStateException("Failed requirement: no cash being redeemed");
|
||||
if (input.getMaturityDate().isAfter(time))
|
||||
|
@ -58,8 +58,8 @@ abstract class TwoPartyTradeProtocol {
|
||||
|
||||
abstract fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer
|
||||
|
||||
abstract class Buyer : ProtocolStateMachine<BuyerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
||||
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
||||
abstract class Buyer : ProtocolStateMachine<BuyerInitialArgs, Pair<WireTransaction, LedgerTransaction>>()
|
||||
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<WireTransaction, LedgerTransaction>>()
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun create(smm: StateMachineManager): TwoPartyTradeProtocol {
|
||||
@ -89,7 +89,7 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
|
||||
// the continuation by the state machine framework. Please refer to the documentation website (docs/build/html) to
|
||||
// learn more about the protocol state machine framework.
|
||||
class SellerImpl : Seller() {
|
||||
override fun call(args: SellerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
override fun call(args: SellerInitialArgs): Pair<WireTransaction, LedgerTransaction> {
|
||||
val sessionID = random63BitValue()
|
||||
|
||||
// Make the first message we'll send to kick off the protocol.
|
||||
@ -99,7 +99,7 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
|
||||
logger().trace { "Received partially signed transaction" }
|
||||
|
||||
partialTX.verifySignatures()
|
||||
val wtx = partialTX.txBits.deserialize<WireTransaction>()
|
||||
val wtx: WireTransaction = partialTX.txBits.deserialize()
|
||||
|
||||
requireThat {
|
||||
"transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(args.myKeyPair.public) == args.price)
|
||||
@ -116,16 +116,15 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
|
||||
// express protocol state machines on top of the messaging layer.
|
||||
}
|
||||
|
||||
val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits.bits)
|
||||
val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits)
|
||||
val fullySigned: SignedWireTransaction = partialTX.copy(sigs = partialTX.sigs + ourSignature)
|
||||
// We should run it through our full TransactionGroup of all transactions here.
|
||||
fullySigned.verify()
|
||||
val timestamped: TimestampedWireTransaction = fullySigned.toTimestampedTransaction(serviceHub.timestampingService)
|
||||
logger().trace { "Built finished transaction, sending back to secondary!" }
|
||||
|
||||
send(TRADE_TOPIC, args.buyerSessionID, timestamped)
|
||||
send(TRADE_TOPIC, args.buyerSessionID, fullySigned)
|
||||
|
||||
return Pair(timestamped, timestamped.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService))
|
||||
return Pair(wtx, fullySigned.verifyToLedgerTransaction(serviceHub.identityService))
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +135,7 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
|
||||
|
||||
// The buyer's side of the protocol. See note above Seller to learn about the caveats here.
|
||||
class BuyerImpl : Buyer() {
|
||||
override fun call(args: BuyerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
||||
override fun call(args: BuyerInitialArgs): Pair<WireTransaction, LedgerTransaction> {
|
||||
// Wait for a trade request to come in on our pre-provided session ID.
|
||||
val tradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, args.sessionID)
|
||||
|
||||
@ -184,16 +183,15 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
|
||||
|
||||
// TODO: Protect against the buyer terminating here and leaving us in the lurch without the final tx.
|
||||
// TODO: Protect against a malicious buyer sending us back a different transaction to the one we built.
|
||||
val fullySigned = sendAndReceive<TimestampedWireTransaction>(TRADE_TOPIC,
|
||||
tradeRequest.sessionID, args.sessionID, stx)
|
||||
val fullySigned = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, tradeRequest.sessionID, args.sessionID, stx)
|
||||
|
||||
logger().trace { "Got fully signed transaction, verifying ... "}
|
||||
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService)
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService)
|
||||
|
||||
logger().trace { "Fully signed transaction was valid. Trade complete! :-)" }
|
||||
|
||||
return Pair(fullySigned, ltx)
|
||||
return Pair(fullySigned.verify(), ltx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,3 +120,10 @@ inline fun <reified T : CommandData> List<AuthenticatedObject<CommandData>>.requ
|
||||
|
||||
// For Java
|
||||
fun List<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<out CommandData>) = filter { klass.isInstance(it) }.single()
|
||||
|
||||
/** Returns a timestamp that was signed by the given authority, or returns null if missing. */
|
||||
fun List<AuthenticatedObject<CommandData>>.getTimestampBy(timestampingAuthority: Party): TimestampCommand? {
|
||||
val timestampCmds = filter { it.signers.contains(timestampingAuthority.owningKey) && it.value is TimestampCommand }
|
||||
return timestampCmds.singleOrNull()?.value as? TimestampCommand
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,6 @@ open class DigitalSignature(bits: ByteArray, val covering: Int = 0) : OpaqueByte
|
||||
}
|
||||
|
||||
class LegallyIdentifiable(val signer: Party, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering)
|
||||
|
||||
}
|
||||
|
||||
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||
@ -90,8 +89,16 @@ fun PrivateKey.signWithECDSA(bits: ByteArray): DigitalSignature {
|
||||
return DigitalSignature(sig)
|
||||
}
|
||||
|
||||
fun PrivateKey.signWithECDSA(bits: ByteArray, publicKey: PublicKey) = DigitalSignature.WithKey(publicKey, signWithECDSA(bits).bits)
|
||||
fun KeyPair.signWithECDSA(bits: ByteArray) = private.signWithECDSA(bits, public)
|
||||
fun PrivateKey.signWithECDSA(bitsToSign: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey {
|
||||
return DigitalSignature.WithKey(publicKey, signWithECDSA(bitsToSign).bits)
|
||||
}
|
||||
fun KeyPair.signWithECDSA(bitsToSign: ByteArray) = private.signWithECDSA(bitsToSign, public)
|
||||
fun KeyPair.signWithECDSA(bitsToSign: OpaqueBytes) = private.signWithECDSA(bitsToSign.bits, public)
|
||||
fun KeyPair.signWithECDSA(bitsToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable {
|
||||
check(public == party.owningKey)
|
||||
val sig = signWithECDSA(bitsToSign)
|
||||
return DigitalSignature.LegallyIdentifiable(party, sig.bits, 0)
|
||||
}
|
||||
|
||||
/** Utility to simplify the act of verifying a signature */
|
||||
fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) {
|
||||
|
@ -9,10 +9,11 @@
|
||||
package core
|
||||
|
||||
import core.messaging.MessagingService
|
||||
import core.serialization.SerializedBytes
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* This file defines various 'services' which are not currently fleshed out. A service is a module that provides
|
||||
@ -70,15 +71,25 @@ interface IdentityService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple interface (for testing) to an abstract timestamping service, in the style of RFC 3161. Note that this is not
|
||||
* 'timestamping' in the block chain sense, but rather, implies a semi-trusted third party taking a reading of the
|
||||
* current time, typically from an atomic clock, and then digitally signing (current time, hash) to produce a timestamp
|
||||
* triple (signature, time, hash). The purpose of these timestamps is to locate a transaction in the timeline, which is
|
||||
* important in the absence of blocks. Here we model the timestamp as an opaque byte array.
|
||||
* Simple interface (for testing) to an abstract timestamping service. Note that this is not "timestamping" in the
|
||||
* blockchain sense of a total ordering of transactions, but rather, a signature from a well known/trusted timestamping
|
||||
* service over a transaction that indicates the timestamp in it is accurate. Such a signature may not always be
|
||||
* necessary: if there are multiple parties involved in a transaction then they can cross-check the timestamp
|
||||
* themselves.
|
||||
*/
|
||||
interface TimestamperService {
|
||||
fun timestamp(hash: SecureHash): ByteArray
|
||||
fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant
|
||||
fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable
|
||||
|
||||
/** The name+pubkey that this timestamper will sign with. */
|
||||
val identity: Party
|
||||
}
|
||||
|
||||
// Smart contracts may wish to specify explicitly which timestamping authorities are trusted to assert the time.
|
||||
// We define a dummy authority here to allow to us to develop prototype contracts in the absence of a real authority.
|
||||
// The timestamper itself is implemented in the unit test part of the code (in TestUtils.kt).
|
||||
object DummyTimestampingAuthority {
|
||||
val key = KeyPairGenerator.getInstance("EC").genKeyPair()
|
||||
val identity = Party("The dummy timestamper", key.public)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -11,6 +11,8 @@ package core
|
||||
import core.serialization.OpaqueBytes
|
||||
import core.serialization.serialize
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* A contract state (or just "state") contains opaque data used by a contract program. It can be thought of as a disk
|
||||
@ -84,6 +86,23 @@ data class AuthenticatedObject<out T : Any>(
|
||||
val value: T
|
||||
)
|
||||
|
||||
/**
|
||||
* If present in a transaction, contains a time that was verified by the timestamping authority/authorities whose
|
||||
* public keys are identified in the containing [Command] object.
|
||||
*/
|
||||
data class TimestampCommand(val after: Instant?, val before: Instant?) : CommandData {
|
||||
init {
|
||||
if (after == null && before == null)
|
||||
throw IllegalArgumentException("At least one of before/after must be specified")
|
||||
if (after != null && before != null)
|
||||
check(after <= before)
|
||||
}
|
||||
|
||||
constructor(time: Instant, tolerance: Duration) : this(time - tolerance, time + tolerance)
|
||||
|
||||
val midpoint: Instant get() = after!! + Duration.between(after, before!!).dividedBy(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for
|
||||
* every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the
|
||||
|
@ -49,7 +49,7 @@ class TransactionGroup(val transactions: Set<LedgerTransaction>, val nonVerified
|
||||
// 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))
|
||||
resolved.add(TransactionForVerification(inputs, tx.outStates, tx.commands, tx.hash))
|
||||
}
|
||||
|
||||
for (tx in resolved)
|
||||
|
@ -8,10 +8,13 @@
|
||||
|
||||
package core
|
||||
|
||||
import core.serialization.*
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
@ -19,26 +22,23 @@ import java.util.*
|
||||
* Views of a transaction as it progresses through the pipeline, from bytes loaded from disk/network to the object
|
||||
* tree passed into a contract.
|
||||
*
|
||||
* 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 arrangement may change in future.
|
||||
*
|
||||
* 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. A WireTransaction can be hashed
|
||||
* in various ways to calculate a *signature hash* (or sighash), this is the hash that is signed by the various involved
|
||||
* keypairs. Note that a sighash is not the same thing as a *transaction id*, which is the hash of a
|
||||
* TimestampedWireTransaction i.e. the outermost serialised form with everything included.
|
||||
* keypairs. Note that a sighash is not the same thing as a *transaction id*, which is the hash of a SignedWireTransaction
|
||||
* i.e. the outermost serialised form with everything included.
|
||||
*
|
||||
* A PartialTransaction is a transaction class that's mutable (unlike the others which are all immutable). It is
|
||||
* intended to be passed around contracts that may edit it by adding new states/commands or modifying the existing set.
|
||||
* Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from
|
||||
* multiple parties.
|
||||
*
|
||||
* 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 party, thus converting
|
||||
* the WireCommand objects into AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party
|
||||
* map. In future it'd make more sense to use a certificate scheme and so that logic would get more complex.
|
||||
* LedgerTransaction is derived from WireTransaction. It is the result of doing some basic key lookups on WireCommand
|
||||
* to see if any keys are from a recognised party, thus converting the WireCommand objects into
|
||||
* AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party 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.
|
||||
*
|
||||
@ -50,19 +50,38 @@ import java.util.*
|
||||
data class WireTransaction(val inputStates: List<ContractStateRef>,
|
||||
val outputStates: List<ContractState>,
|
||||
val commands: List<Command>) {
|
||||
fun toLedgerTransaction(timestamp: Instant?, identityService: IdentityService, originalHash: SecureHash): LedgerTransaction {
|
||||
fun toLedgerTransaction(identityService: IdentityService, originalHash: SecureHash): LedgerTransaction {
|
||||
val authenticatedArgs = commands.map {
|
||||
val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) }
|
||||
AuthenticatedObject(it.pubkeys, institutions, it.data)
|
||||
}
|
||||
return LedgerTransaction(inputStates, outputStates, authenticatedArgs, timestamp, originalHash)
|
||||
return LedgerTransaction(inputStates, outputStates, authenticatedArgs, originalHash)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown if an attempt is made to timestamp a transaction using a trusted timestamper, but the time on the transaction
|
||||
* is too far in the past or future relative to the local clock and thus the timestamper would reject it.
|
||||
*/
|
||||
class TooLateException : Exception()
|
||||
|
||||
/** A mutable transaction that's in the process of being built, before all signatures are present. */
|
||||
class PartialTransaction(private val inputStates: MutableList<ContractStateRef> = arrayListOf(),
|
||||
private val outputStates: MutableList<ContractState> = arrayListOf(),
|
||||
private val commands: MutableList<Command> = arrayListOf()) {
|
||||
|
||||
val time: TimestampCommand? get() = commands.mapNotNull { it.data as? TimestampCommand }.singleOrNull()
|
||||
|
||||
/**
|
||||
* Places a [TimestampCommand] in this transaction, removing any existing command if there is one.
|
||||
* To get the right signature from the timestamping service, use the [timestamp] method.
|
||||
*/
|
||||
fun setTime(time: Instant, authenticatedBy: Party) {
|
||||
check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" }
|
||||
commands.removeAll { it.data is TimestampCommand }
|
||||
addCommand(TimestampCommand(time, 30.seconds), authenticatedBy.owningKey)
|
||||
}
|
||||
|
||||
/** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
|
||||
public fun withItems(vararg items: Any): PartialTransaction {
|
||||
for (t in items) {
|
||||
@ -83,16 +102,45 @@ class PartialTransaction(private val inputStates: MutableList<ContractStateRef>
|
||||
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
|
||||
check(commands.count { it.pubkeys.contains(key.public) } > 0) { "Trying to sign with a key that isn't in any command" }
|
||||
val data = toWireTransaction().serialize()
|
||||
currentSigs.add(key.private.signWithECDSA(data.bits, key.public))
|
||||
currentSigs.add(key.signWithECDSA(data.bits))
|
||||
}
|
||||
|
||||
fun toWireTransaction() = WireTransaction(inputStates, outputStates, commands)
|
||||
/**
|
||||
* Uses the given timestamper service to request a signature over the WireTransaction be added. There must always be
|
||||
* at least one such signature, but others may be added as well. You may want to have multiple redundant timestamps
|
||||
* in the following cases:
|
||||
*
|
||||
* - Cross border contracts where local law says that only local timestamping authorities are acceptable.
|
||||
* - Backup in case a TSA's signing key is compromised.
|
||||
*
|
||||
* The signature of the trusted timestamper merely asserts that the time field of this transaction is valid.
|
||||
*/
|
||||
fun timestamp(timestamper: TimestamperService) {
|
||||
// TODO: Once we switch to a more advanced bytecode rewriting framework, we can call into a real implementation.
|
||||
check(timestamper.javaClass.simpleName == "DummyTimestamper")
|
||||
val t = time ?: throw IllegalStateException("Timestamping requested but no time was inserted into the transaction")
|
||||
|
||||
// Obviously this is just a hard-coded dummy value for now.
|
||||
val maxExpectedLatency = 5.seconds
|
||||
if (Duration.between(Instant.now(), t.before) > maxExpectedLatency)
|
||||
throw TooLateException()
|
||||
|
||||
// The timestamper may also throw TooLateException if our clocks are desynchronised or if we are right on the
|
||||
// boundary of t.notAfter and network latency pushes us over the edge. By "synchronised" here we mean relative
|
||||
// to GPS time i.e. the United States Naval Observatory.
|
||||
val sig = timestamper.timestamp(toWireTransaction().serialize())
|
||||
currentSigs.add(sig)
|
||||
}
|
||||
|
||||
fun toWireTransaction() = WireTransaction(ArrayList(inputStates), ArrayList(outputStates), ArrayList(commands))
|
||||
|
||||
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedWireTransaction {
|
||||
if (checkSufficientSignatures) {
|
||||
val requiredKeys = commands.flatMap { it.pubkeys }.toSet()
|
||||
val gotKeys = currentSigs.map { it.by }.toSet()
|
||||
check(gotKeys == requiredKeys) { "The set of required signatures isn't equal to the signatures we've got" }
|
||||
for (command in commands) {
|
||||
if (!gotKeys.containsAll(command.pubkeys))
|
||||
throw IllegalStateException("Missing signatures on the transaction for a ${command.data.javaClass.canonicalName} command")
|
||||
}
|
||||
}
|
||||
return SignedWireTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
|
||||
}
|
||||
@ -153,43 +201,17 @@ data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, v
|
||||
// unverified.
|
||||
val cmdKeys = wtx.commands.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")
|
||||
if (!sigKeys.containsAll(cmdKeys))
|
||||
throw SignatureException("Missing signatures on the transaction for: ${cmdKeys - sigKeys}")
|
||||
return wtx
|
||||
}
|
||||
|
||||
/** Uses the given timestamper service to calculate a signed timestamp and then returns a wrapper for both */
|
||||
fun toTimestampedTransaction(timestamper: TimestamperService): TimestampedWireTransaction {
|
||||
val bits = serialize()
|
||||
return TimestampedWireTransaction(bits, timestamper.timestamp(bits.sha256()).opaque())
|
||||
}
|
||||
|
||||
/** Returns a [TimestampedWireTransaction] with an empty byte array as the timestamp: this means, no time was provided. */
|
||||
fun toTimestampedTransactionWithoutTime() = TimestampedWireTransaction(serialize(), null)
|
||||
}
|
||||
|
||||
/**
|
||||
* A TimestampedWireTransaction is the outermost, final form that a transaction takes. The hash of this structure is
|
||||
* how transactions are identified on the network and in the ledger.
|
||||
*/
|
||||
data class TimestampedWireTransaction(
|
||||
/** A serialised SignedWireTransaction */
|
||||
val signedWireTXBytes: SerializedBytes<SignedWireTransaction>,
|
||||
|
||||
/** Signature from a timestamping authority. For instance using RFC 3161 */
|
||||
val timestamp: OpaqueBytes?
|
||||
) {
|
||||
val transactionID: SecureHash = serialize().sha256()
|
||||
|
||||
fun verifyToLedgerTransaction(timestamper: TimestamperService, identityService: IdentityService): LedgerTransaction {
|
||||
val stx = signedWireTXBytes.deserialize()
|
||||
val wtx: WireTransaction = stx.verify()
|
||||
val instant: Instant? =
|
||||
if (timestamp != null)
|
||||
timestamper.verifyTimestamp(signedWireTXBytes.sha256(), timestamp.bits)
|
||||
else
|
||||
null
|
||||
return wtx.toLedgerTransaction(instant, identityService, transactionID)
|
||||
/**
|
||||
* Calls [verify] to check all required signatures are present, and then uses the passed [IdentityService] to call
|
||||
* [WireTransaction.toLedgerTransaction] to look up well known identities from pubkeys.
|
||||
*/
|
||||
fun verifyToLedgerTransaction(identityService: IdentityService): LedgerTransaction {
|
||||
return verify().toLedgerTransaction(identityService, txBits.bits.sha256())
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,11 +227,8 @@ data class LedgerTransaction(
|
||||
val outStates: List<ContractState>,
|
||||
/** Arbitrary data passed to the program of each input state. */
|
||||
val commands: List<AuthenticatedObject<CommandData>>,
|
||||
/** The moment the transaction was timestamped for, if a timestamp was present. */
|
||||
val time: Instant?,
|
||||
/** The hash of the original serialised TimestampedWireTransaction or SignedTransaction */
|
||||
val hash: SecureHash
|
||||
// TODO: nLockTime equivalent?
|
||||
) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : ContractState> outRef(index: Int) = StateAndRef(outStates[index] as T, ContractStateRef(hash, index))
|
||||
@ -222,11 +241,11 @@ data class LedgerTransaction(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move class this into TransactionGroup.kt
|
||||
/** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */
|
||||
data class TransactionForVerification(val inStates: List<ContractState>,
|
||||
val outStates: List<ContractState>,
|
||||
val commands: List<AuthenticatedObject<CommandData>>,
|
||||
val time: Instant?,
|
||||
val origHash: SecureHash) {
|
||||
override fun hashCode() = origHash.hashCode()
|
||||
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
|
||||
@ -256,6 +275,9 @@ data class TransactionForVerification(val inStates: List<ContractState>,
|
||||
|
||||
data class InOutGroup<T : ContractState>(val inputs: List<T>, val outputs: List<T>)
|
||||
|
||||
// A shortcut to make IDE auto-completion more intuitive for Java users.
|
||||
fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority)
|
||||
|
||||
// For Java users.
|
||||
fun <T : ContractState> groupStates(ofType: Class<T>, selector: (T) -> Any): List<InOutGroup<T>> {
|
||||
val inputs = inStates.filterIsInstance(ofType)
|
||||
|
@ -39,6 +39,7 @@ class CommercialPaperTests {
|
||||
transaction {
|
||||
output { PAPER_1 }
|
||||
arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "signed by the claimed issuer")
|
||||
@ -51,6 +52,7 @@ class CommercialPaperTests {
|
||||
transaction {
|
||||
output { PAPER_1.copy(faceValue = 0.DOLLARS) }
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "face value is not zero")
|
||||
@ -63,6 +65,7 @@ class CommercialPaperTests {
|
||||
transaction {
|
||||
output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) }
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "maturity date is not in the past")
|
||||
@ -79,6 +82,7 @@ class CommercialPaperTests {
|
||||
input("paper")
|
||||
output { PAPER_1 }
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "there is no input state")
|
||||
@ -96,7 +100,7 @@ class CommercialPaperTests {
|
||||
}
|
||||
|
||||
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
|
||||
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), TEST_TX_TIME, SecureHash.randomSHA256())
|
||||
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
|
||||
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, ContractStateRef(ltx.hash, index)) })
|
||||
}
|
||||
|
||||
@ -104,10 +108,13 @@ class CommercialPaperTests {
|
||||
fun `issue move and then redeem`() {
|
||||
// MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
|
||||
val issueTX: LedgerTransaction = run {
|
||||
val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply {
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity)
|
||||
signWith(MINI_CORP_KEY)
|
||||
timestamp(DUMMY_TIMESTAMPER)
|
||||
}
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256())
|
||||
stx.verifyToLedgerTransaction(MockIdentityService)
|
||||
}
|
||||
|
||||
val (alicesWalletTX, alicesWallet) = cashOutputsToWallet(
|
||||
@ -124,7 +131,7 @@ class CommercialPaperTests {
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256())
|
||||
stx.verifyToLedgerTransaction(MockIdentityService)
|
||||
}
|
||||
|
||||
// Won't be validated.
|
||||
@ -135,10 +142,12 @@ class CommercialPaperTests {
|
||||
|
||||
fun makeRedeemTX(time: Instant): LedgerTransaction {
|
||||
val ptx = PartialTransaction()
|
||||
ptx.setTime(time, DummyTimestampingAuthority.identity )
|
||||
CommercialPaper().craftRedeem(ptx, moveTX.outRef(1), corpWallet)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
return ptx.toSignedTransaction().verify().toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256())
|
||||
ptx.timestamp(DUMMY_TIMESTAMPER)
|
||||
return ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService)
|
||||
}
|
||||
|
||||
val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)
|
||||
@ -154,8 +163,8 @@ class CommercialPaperTests {
|
||||
|
||||
// Generate a trade lifecycle with various parameters.
|
||||
fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
|
||||
aliceGetsBack: Amount = 1000.DOLLARS,
|
||||
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<CommercialPaper.State> {
|
||||
aliceGetsBack: Amount = 1000.DOLLARS,
|
||||
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<CommercialPaper.State> {
|
||||
val someProfits = 1200.DOLLARS
|
||||
return transactionGroupFor() {
|
||||
roots {
|
||||
@ -167,6 +176,7 @@ class CommercialPaperTests {
|
||||
transaction("Issuance") {
|
||||
output("paper") { PAPER_1 }
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
// The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
|
||||
@ -182,7 +192,7 @@ class CommercialPaperTests {
|
||||
|
||||
// Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
|
||||
// as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
|
||||
transaction("Redemption", redemptionTime) {
|
||||
transaction("Redemption") {
|
||||
input("alice's paper")
|
||||
input("some profits")
|
||||
|
||||
@ -193,6 +203,8 @@ class CommercialPaperTests {
|
||||
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(ALICE) { CommercialPaper.Commands.Redeem() }
|
||||
|
||||
timestamp(redemptionTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ class CrowdFundTests {
|
||||
transaction {
|
||||
output { CF_1 }
|
||||
arg(DUMMY_PUBKEY_1) { CrowdFund.Commands.Register() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "the transaction is signed by the owner of the crowdsourcing")
|
||||
@ -46,6 +47,7 @@ class CrowdFundTests {
|
||||
transaction {
|
||||
output { CF_1.copy(campaign = CF_1.campaign.copy(closingTime = TEST_TX_TIME - 1.days)) }
|
||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "the output registration has a closing time in the future")
|
||||
@ -67,6 +69,7 @@ class CrowdFundTests {
|
||||
transaction {
|
||||
output("funding opportunity") { CF_1 }
|
||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
// 2. Place a pledge
|
||||
@ -81,19 +84,21 @@ class CrowdFundTests {
|
||||
output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY }
|
||||
arg(ALICE) { Cash.Commands.Move() }
|
||||
arg(ALICE) { CrowdFund.Commands.Pledge() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
// 3. Close the opportunity, assuming the target has been met
|
||||
transaction(time = TEST_TX_TIME + 8.days) {
|
||||
transaction {
|
||||
input ("pledged opportunity")
|
||||
output ("funded and closed") { "pledged opportunity".output.copy(closed = true) }
|
||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Close() }
|
||||
timestamp(time = TEST_TX_TIME + 8.days)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
|
||||
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), TEST_TX_TIME, SecureHash.randomSHA256())
|
||||
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
|
||||
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, ContractStateRef(ltx.hash, index)) })
|
||||
}
|
||||
|
||||
@ -102,10 +107,13 @@ class CrowdFundTests {
|
||||
// MiniCorp registers a crowdfunding of $1,000, to close in 7 days.
|
||||
val registerTX: LedgerTransaction = run {
|
||||
// craftRegister returns a partial transaction
|
||||
val ptx = CrowdFund().craftRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
val ptx = CrowdFund().craftRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days).apply {
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity)
|
||||
signWith(MINI_CORP_KEY)
|
||||
timestamp(DUMMY_TIMESTAMPER)
|
||||
}
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256())
|
||||
stx.verifyToLedgerTransaction(MockIdentityService)
|
||||
}
|
||||
|
||||
// let's give Alice some funds that she can invest
|
||||
@ -120,10 +128,12 @@ class CrowdFundTests {
|
||||
val ptx = PartialTransaction()
|
||||
CrowdFund().craftPledge(ptx, registerTX.outRef(0), ALICE)
|
||||
Cash().craftSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet)
|
||||
ptx.setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.timestamp(DUMMY_TIMESTAMPER)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
// this verify passes - the transaction contains an output cash, necessary to verify the fund command
|
||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256())
|
||||
stx.verifyToLedgerTransaction(MockIdentityService)
|
||||
}
|
||||
|
||||
// Won't be validated.
|
||||
@ -134,10 +144,12 @@ class CrowdFundTests {
|
||||
// MiniCorp closes their campaign.
|
||||
fun makeFundedTX(time: Instant): LedgerTransaction {
|
||||
val ptx = PartialTransaction()
|
||||
ptx.setTime(time, DUMMY_TIMESTAMPER.identity)
|
||||
CrowdFund().craftClose(ptx, pledgeTX.outRef(0), miniCorpWallet)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.timestamp(DUMMY_TIMESTAMPER)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
return stx.verify().toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256())
|
||||
return stx.verifyToLedgerTransaction(MockIdentityService)
|
||||
}
|
||||
|
||||
val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days)
|
||||
@ -150,7 +162,5 @@ class CrowdFundTests {
|
||||
|
||||
// This verification passes
|
||||
TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify(TEST_PROGRAM_MAP)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -97,7 +97,7 @@ class TransactionGroupTests {
|
||||
// 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())
|
||||
listOf(ref), listOf(A_THOUSAND_POUNDS), listOf(AuthenticatedObject(listOf(BOB), emptyList(), Cash.Commands.Move())), SecureHash.randomSHA256())
|
||||
)
|
||||
|
||||
val e = assertFailsWith(TransactionResolutionException::class) {
|
||||
|
@ -16,7 +16,6 @@ import org.junit.Test
|
||||
import java.security.SignatureException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class TransactionSerializationTests {
|
||||
// Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change).
|
||||
@ -86,18 +85,14 @@ class TransactionSerializationTests {
|
||||
|
||||
@Test
|
||||
fun timestamp() {
|
||||
tx.setTime(TEST_TX_TIME, DUMMY_TIMESTAMPER.identity)
|
||||
tx.timestamp(DUMMY_TIMESTAMPER)
|
||||
tx.signWith(TestUtils.keypair)
|
||||
val ttx = tx.toSignedTransaction().toTimestampedTransactionWithoutTime()
|
||||
val ltx = ttx.verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService)
|
||||
val stx = tx.toSignedTransaction()
|
||||
val ltx = stx.verifyToLedgerTransaction(MockIdentityService)
|
||||
assertEquals(tx.commands().map { it.data }, ltx.commands.map { it.value })
|
||||
assertEquals(tx.inputStates(), ltx.inStateRefs)
|
||||
assertEquals(tx.outputStates(), ltx.outStates)
|
||||
assertNull(ltx.time)
|
||||
|
||||
val ltx2: LedgerTransaction = tx.
|
||||
toSignedTransaction().
|
||||
toTimestampedTransaction(DUMMY_TIMESTAMPER).
|
||||
verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService)
|
||||
assertEquals(TEST_TX_TIME, ltx2.time)
|
||||
assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_TIMESTAMPER.identity)!!.midpoint)
|
||||
}
|
||||
}
|
@ -10,20 +10,20 @@
|
||||
|
||||
package core.testutils
|
||||
|
||||
import com.google.common.io.BaseEncoding
|
||||
import contracts.*
|
||||
import core.*
|
||||
import core.messaging.MessagingService
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
import core.visualiser.GraphVisualiser
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.test.assertEquals
|
||||
@ -67,27 +67,19 @@ val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
|
||||
)
|
||||
|
||||
/**
|
||||
* A test/mock timestamping service that doesn't use any signatures or security. It always timestamps with
|
||||
* [TEST_TX_TIME], an arbitrary point on the timeline.
|
||||
* A test/mock timestamping service that doesn't use any signatures or security. It timestamps with
|
||||
* the provided clock which defaults to [TEST_TX_TIME], an arbitrary point on the timeline.
|
||||
*/
|
||||
class DummyTimestamper(private val time: Instant = TEST_TX_TIME) : TimestamperService {
|
||||
override fun timestamp(hash: SecureHash): ByteArray {
|
||||
val bos = ByteArrayOutputStream()
|
||||
DataOutputStream(bos).use {
|
||||
it.writeLong(time.toEpochMilli())
|
||||
it.write(hash.bits)
|
||||
}
|
||||
return bos.toByteArray()
|
||||
}
|
||||
class DummyTimestamper(var clock: Clock = Clock.fixed(TEST_TX_TIME, ZoneId.systemDefault()),
|
||||
val tolerance: Duration = 30.seconds) : TimestamperService {
|
||||
override val identity = DummyTimestampingAuthority.identity
|
||||
|
||||
override fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant {
|
||||
val dis = DataInputStream(ByteArrayInputStream(signedTimestamp))
|
||||
val epochMillis = dis.readLong()
|
||||
val serHash = ByteArray(32)
|
||||
dis.readFully(serHash)
|
||||
if (!Arrays.equals(serHash, hash.bits))
|
||||
throw IllegalStateException("Hash mismatch: ${BaseEncoding.base16().encode(serHash)} vs ${BaseEncoding.base16().encode(hash.bits)}")
|
||||
return Instant.ofEpochMilli(epochMillis)
|
||||
override fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable {
|
||||
val wtx = wtxBytes.deserialize()
|
||||
val timestamp = wtx.commands.mapNotNull { it.data as? TimestampCommand }.single()
|
||||
if (Duration.between(timestamp.before, clock.instant()) > tolerance)
|
||||
throw TooLateException()
|
||||
return DummyTimestampingAuthority.key.signWithECDSA(wtxBytes.bits, identity)
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,6 +181,10 @@ abstract class AbstractTransactionForTest {
|
||||
commands.add(Command(c(), keys))
|
||||
}
|
||||
|
||||
fun timestamp(time: Instant) {
|
||||
commands.add(Command(TimestampCommand(time, 30.seconds), DUMMY_TIMESTAMPER.identity.owningKey))
|
||||
}
|
||||
|
||||
// Forbid patterns like: transaction { ... transaction { ... } }
|
||||
@Deprecated("Cannot nest transactions, use tweak", level = DeprecationLevel.ERROR)
|
||||
fun transaction(body: TransactionForTest.() -> Unit) {}
|
||||
@ -200,8 +196,8 @@ open class TransactionForTest : AbstractTransactionForTest() {
|
||||
fun input(s: () -> ContractState) = inStates.add(s())
|
||||
|
||||
protected fun run(time: Instant) {
|
||||
val tx = TransactionForVerification(inStates, outStates.map { it.state }, commandsToAuthenticatedObjects(),
|
||||
time, SecureHash.randomSHA256())
|
||||
val cmds = commandsToAuthenticatedObjects()
|
||||
val tx = TransactionForVerification(inStates, outStates.map { it.state }, cmds, SecureHash.randomSHA256())
|
||||
tx.verify(TEST_PROGRAM_MAP)
|
||||
}
|
||||
|
||||
@ -281,12 +277,12 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
|
||||
|
||||
/**
|
||||
* Converts to a [LedgerTransaction] with the givn time, the test institution map, and just assigns a random
|
||||
* hash (i.e. pretend it was signed)
|
||||
* Converts to a [LedgerTransaction] with the test institution map, and just assigns a random hash
|
||||
* (i.e. pretend it was signed)
|
||||
*/
|
||||
fun toLedgerTransaction(time: Instant): LedgerTransaction {
|
||||
fun toLedgerTransaction(): LedgerTransaction {
|
||||
val wtx = WireTransaction(inStates, outStates.map { it.state }, commands)
|
||||
return wtx.toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256())
|
||||
return wtx.toLedgerTransaction(MockIdentityService, SecureHash.randomSHA256())
|
||||
}
|
||||
}
|
||||
|
||||
@ -296,8 +292,8 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
fun <C : ContractState> lookup(label: String) = StateAndRef(label.output as C, label.outputRef)
|
||||
|
||||
private inner class InternalLedgerTransactionDSL : LedgerTransactionDSL() {
|
||||
fun finaliseAndInsertLabels(time: Instant): LedgerTransaction {
|
||||
val ltx = toLedgerTransaction(time)
|
||||
fun finaliseAndInsertLabels(): LedgerTransaction {
|
||||
val ltx = toLedgerTransaction()
|
||||
for ((index, labelledState) in outStates.withIndex()) {
|
||||
if (labelledState.label != null) {
|
||||
labelToRefs[labelledState.label] = ContractStateRef(ltx.hash, index)
|
||||
@ -322,7 +318,7 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
fun transaction(vararg outputStates: LabeledOutput) {
|
||||
val outs = outputStates.map { it.state }
|
||||
val wtx = WireTransaction(emptyList(), outs, emptyList())
|
||||
val ltx = wtx.toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256())
|
||||
val ltx = wtx.toLedgerTransaction(MockIdentityService, SecureHash.randomSHA256())
|
||||
for ((index, state) in outputStates.withIndex()) {
|
||||
val label = state.label!!
|
||||
labelToRefs[label] = ContractStateRef(ltx.hash, index)
|
||||
@ -335,17 +331,17 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
||||
fun roots(body: Roots.() -> Unit) {}
|
||||
@Deprecated("Use the vararg form of transaction inside roots", level = DeprecationLevel.ERROR)
|
||||
fun transaction(time: Instant = TEST_TX_TIME, body: LedgerTransactionDSL.() -> Unit) {}
|
||||
fun transaction(body: LedgerTransactionDSL.() -> Unit) {}
|
||||
}
|
||||
fun roots(body: Roots.() -> Unit) = Roots().apply { body() }
|
||||
|
||||
val txns = ArrayList<LedgerTransaction>()
|
||||
private val txnToLabelMap = HashMap<LedgerTransaction, String>()
|
||||
|
||||
fun transaction(label: String? = null, time: Instant = TEST_TX_TIME, body: LedgerTransactionDSL.() -> Unit): LedgerTransaction {
|
||||
fun transaction(label: String? = null, body: LedgerTransactionDSL.() -> Unit): LedgerTransaction {
|
||||
val forTest = InternalLedgerTransactionDSL()
|
||||
forTest.body()
|
||||
val ltx = forTest.finaliseAndInsertLabels(time)
|
||||
val ltx = forTest.finaliseAndInsertLabels()
|
||||
txns.add(ltx)
|
||||
if (label != null)
|
||||
txnToLabelMap[ltx] = label
|
||||
|
Loading…
x
Reference in New Issue
Block a user