mirror of
https://github.com/corda/corda.git
synced 2024-12-20 05:28:21 +00:00
Merged tx-simplifications into master
This commit is contained in:
commit
c217702606
@ -18,7 +18,7 @@ are created and destroyed by digitally signed **transactions**. Each transaction
|
||||
consume/destroy, these are called **inputs**, and contains a set of new states that it will create, these are called
|
||||
**outputs**.
|
||||
|
||||
States contain arbitrary data, but they always contain at minimum a pointer to the bytecode of a
|
||||
States contain arbitrary data, but they always contain at minimum a hash of the bytecode of a
|
||||
**code contract**, which is a program expressed in some byte code that runs sandboxed inside a virtual machine. Code
|
||||
contracts (or just "contracts" in the rest of this document) are globally shared pieces of business logic. Contracts
|
||||
define a **verify function**, which is a pure function given the entire transaction as input.
|
||||
@ -26,26 +26,21 @@ define a **verify function**, which is a pure function given the entire transact
|
||||
To be considered valid, the transaction must be **accepted** by the verify function of every contract pointed to by the
|
||||
input and output states. Beyond inputs and outputs, transactions may also contain **commands**, small data packets that
|
||||
the platform does not interpret itself, but which can parameterise execution of the contracts. They can be thought of as
|
||||
arguments to the verify function.
|
||||
arguments to the verify function. Each command has a list of **public keys** associated with it. The platform ensures
|
||||
that the transaction is signed by every key listed in the commands before the contracts start to execute. Public keys
|
||||
may be random/identityless for privacy, or linked to a well known legal identity via a *public key infrastructure* (PKI).
|
||||
|
||||
Note that there is nothing that explicitly binds together specific inputs, outputs or commands. Instead it's up to the
|
||||
contract code to interpret the pieces inside the transaction and ensure they fit together correctly. This is done to
|
||||
maximise flexibility for the contract developer.
|
||||
|
||||
A transaction has one or more **signatures** attached to it. The signatures do not mean anything by themselves, rather,
|
||||
their existence is given as input to the contract which can then decide which set of signatures it demands (if any).
|
||||
Signatures may be from an arbitrary, random **public key** that has no identity attached. A public key may be
|
||||
well known, that is, appears in some sort of public identity registry. In this case we say the key is owned by a
|
||||
**party**, which is defined (for now) as being merely a (public key, name) pair.
|
||||
|
||||
A transaction may also be **timestamped**. A timestamp is a (hash, datetime, signature) triple from a
|
||||
*timestamping authority* (TSA). The notion of a TSA is not ledger specific and is defined by
|
||||
`IETF RFC 3161 <https://www.ietf.org/rfc/rfc3161.txt>`_ which defines the internet standard Timestamping Protocol (TSP).
|
||||
The purpose of the TSA is to attach a single, globally agreed upon time which a contract may use to enforce certain
|
||||
types of time-based logic. The TSA's do not need to know about the contents of the transaction in order to provide a
|
||||
timestamp, and they are therefore never exposed to private data.
|
||||
|
||||
.. note:: In the current code, use of TSAs is not implemented.
|
||||
Transactions may sometimes need to provide a contract with data from the outside world. Examples may include stock
|
||||
prices, facts about events or the statuses of legal entities (e.g. bankruptcy), and so on. The providers of such
|
||||
facts are called **oracles** and they provide facts to the ledger by signing transactions that contain commands they
|
||||
recognise. The commands contain the fact and the signature shows agreement to that fact. Time is also modelled as
|
||||
a fact, with the signature of a special kind of oracle called a **timestamping authority** (TSA). A TSA signs
|
||||
a transaction if a pre-defined timestamping command in it defines a after/before time window that includes "true
|
||||
time" (i.e. GPS time as calibrated to the US Naval Observatory).
|
||||
|
||||
As the same terminology often crops up in different distributed ledger designs, let's compare this to other
|
||||
distributed ledger systems you may be familiar with. You can find more detailed design rationales for why the platform
|
||||
|
@ -66,8 +66,8 @@ class Cash : Contract {
|
||||
}
|
||||
|
||||
// Just for grouping
|
||||
interface Commands : Command {
|
||||
class Move() : TypeOnlyCommand(), Commands
|
||||
interface Commands : CommandData {
|
||||
class Move() : TypeOnlyCommandData(), Commands
|
||||
|
||||
/**
|
||||
* Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction
|
||||
@ -155,7 +155,7 @@ class Cash : Contract {
|
||||
check(tx.inputStates().isEmpty())
|
||||
check(tx.outputStates().sumCashOrNull() == null)
|
||||
tx.addOutputState(Cash.State(at, amount, owner))
|
||||
tx.addArg(WireCommand(Cash.Commands.Issue(), listOf(at.party.owningKey)))
|
||||
tx.addCommand(Cash.Commands.Issue(), at.party.owningKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -233,7 +233,7 @@ class Cash : Contract {
|
||||
for (state in outputs) tx.addOutputState(state)
|
||||
// What if we already have a move command with the right keys? Filter it out here or in platform code?
|
||||
val keysList = keysUsed.toList()
|
||||
tx.addArg(WireCommand(Commands.Move(), keysList))
|
||||
tx.addCommand(Commands.Move(), keysList)
|
||||
return keysList
|
||||
}
|
||||
}
|
||||
|
@ -53,12 +53,12 @@ class CommercialPaper : Contract {
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
}
|
||||
|
||||
interface Commands : Command {
|
||||
class Move : TypeOnlyCommand(), Commands
|
||||
class Redeem : TypeOnlyCommand(), Commands
|
||||
interface Commands : CommandData {
|
||||
class Move : TypeOnlyCommandData(), Commands
|
||||
class Redeem : TypeOnlyCommandData(), Commands
|
||||
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
|
||||
// However, nothing in the platform enforces that uniqueness: it's up to the issuer.
|
||||
class Issue : TypeOnlyCommand(), Commands
|
||||
class Issue : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForVerification) {
|
||||
@ -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
|
||||
@ -119,7 +119,7 @@ class CommercialPaper : Contract {
|
||||
*/
|
||||
fun craftIssue(issuance: PartyReference, faceValue: Amount, maturityDate: Instant): PartialTransaction {
|
||||
val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate)
|
||||
return PartialTransaction().withItems(state, WireCommand(Commands.Issue(), issuance.party.owningKey))
|
||||
return PartialTransaction().withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -128,7 +128,7 @@ class CommercialPaper : Contract {
|
||||
fun craftMove(tx: PartialTransaction, paper: StateAndRef<State>, newOwner: PublicKey) {
|
||||
tx.addInputState(paper.ref)
|
||||
tx.addOutputState(paper.state.copy(owner = newOwner))
|
||||
tx.addArg(WireCommand(Commands.Move(), paper.state.owner))
|
||||
tx.addCommand(Commands.Move(), paper.state.owner)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -143,7 +143,7 @@ class CommercialPaper : Contract {
|
||||
// Add the cash movement using the states in our wallet.
|
||||
Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet)
|
||||
tx.addInputState(paper.ref)
|
||||
tx.addArg(WireCommand(CommercialPaper.Commands.Redeem(), paper.state.owner))
|
||||
tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.owner)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,10 +66,10 @@ class CrowdFund : Contract {
|
||||
)
|
||||
|
||||
|
||||
interface Commands : Command {
|
||||
class Register : TypeOnlyCommand(), Commands
|
||||
class Pledge : TypeOnlyCommand(), Commands
|
||||
class Close : TypeOnlyCommand(), Commands
|
||||
interface Commands : CommandData {
|
||||
class Register : TypeOnlyCommandData(), Commands
|
||||
class Pledge : TypeOnlyCommandData(), Commands
|
||||
class Close : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForVerification) {
|
||||
@ -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)
|
||||
@ -146,7 +145,7 @@ class CrowdFund : Contract {
|
||||
fun craftRegister(owner: PartyReference, fundingTarget: Amount, fundingName: String, closingTime: Instant): PartialTransaction {
|
||||
val campaign = Campaign(owner = owner.party.owningKey, name = fundingName, target = fundingTarget, closingTime = closingTime)
|
||||
val state = State(campaign)
|
||||
return PartialTransaction().withItems(state, WireCommand(Commands.Register(), owner.party.owningKey))
|
||||
return PartialTransaction().withItems(state, Command(Commands.Register(), owner.party.owningKey))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -157,13 +156,13 @@ class CrowdFund : Contract {
|
||||
tx.addOutputState(campaign.state.copy(
|
||||
pledges = campaign.state.pledges + CrowdFund.Pledge(subscriber, 1000.DOLLARS)
|
||||
))
|
||||
tx.addArg(WireCommand(Commands.Pledge(), subscriber))
|
||||
tx.addCommand(Commands.Pledge(), subscriber)
|
||||
}
|
||||
|
||||
fun craftClose(tx: PartialTransaction, campaign: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) {
|
||||
tx.addInputState(campaign.ref)
|
||||
tx.addOutputState(campaign.state.copy(closed = true))
|
||||
tx.addArg(WireCommand(Commands.Close(), campaign.state.campaign.owner))
|
||||
tx.addCommand(Commands.Close(), campaign.state.campaign.owner)
|
||||
// If campaign target has not been met, compose cash returns
|
||||
if (campaign.state.pledgedAmount < campaign.state.campaign.target) {
|
||||
for (pledge in campaign.state.pledges) {
|
||||
|
@ -91,7 +91,7 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Commands implements core.Command {
|
||||
public static class Commands implements core.CommandData {
|
||||
public static class Move extends Commands {
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
@ -114,9 +114,12 @@ public class JavaCommercialPaper implements Contract {
|
||||
List<InOutGroup<State>> groups = tx.groupStates(State.class, State::withoutOwner);
|
||||
|
||||
// Find the command that instructs us what to do and check there's exactly one.
|
||||
AuthenticatedObject<Command> cmd = requireSingleCommand(tx.getCommands(), Commands.class);
|
||||
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)
|
||||
|
||||
@ -167,7 +166,7 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
|
||||
val freshKey = serviceHub.keyManagementService.freshKey()
|
||||
val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public)
|
||||
ptx.addOutputState(state)
|
||||
ptx.addArg(WireCommand(command, tradeRequest.assetForSale.state.owner))
|
||||
ptx.addCommand(command, tradeRequest.assetForSale.state.owner)
|
||||
|
||||
// Now sign the transaction with whatever keys we need to move the cash.
|
||||
for (k in cashSigningPubKeys) {
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,17 +106,24 @@ fun Iterable<Amount>.sumOrZero(currency: Currency) = if (iterator().hasNext()) s
|
||||
//// Authenticated commands ///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/** Filters the command list by type, party and public key all at once. */
|
||||
inline fun <reified T : Command> List<AuthenticatedObject<Command>>.select(signer: PublicKey? = null, party: Party? = null) =
|
||||
inline fun <reified T : CommandData> List<AuthenticatedObject<CommandData>>.select(signer: PublicKey? = null, party: Party? = null) =
|
||||
filter { it.value is T }.
|
||||
filter { if (signer == null) true else it.signers.contains(signer) }.
|
||||
filter { if (party == null) true else it.signingParties.contains(party) }.
|
||||
map { AuthenticatedObject<T>(it.signers, it.signingParties, it.value as T) }
|
||||
|
||||
inline fun <reified T : Command> List<AuthenticatedObject<Command>>.requireSingleCommand() = try {
|
||||
inline fun <reified T : CommandData> List<AuthenticatedObject<CommandData>>.requireSingleCommand() = try {
|
||||
select<T>().single()
|
||||
} catch (e: NoSuchElementException) {
|
||||
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
|
||||
}
|
||||
|
||||
// For Java
|
||||
fun List<AuthenticatedObject<Command>>.requireSingleCommand(klass: Class<out Command>) = filter { klass.isInstance(it) }.single()
|
||||
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
|
||||
@ -31,7 +33,7 @@ interface OwnableState : ContractState {
|
||||
val owner: PublicKey
|
||||
|
||||
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
|
||||
fun withNewOwner(newOwner: PublicKey): Pair<Command, OwnableState>
|
||||
fun withNewOwner(newOwner: PublicKey): Pair<CommandData, OwnableState>
|
||||
}
|
||||
|
||||
/** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */
|
||||
@ -63,14 +65,19 @@ data class PartyReference(val party: Party, val reference: OpaqueBytes) {
|
||||
}
|
||||
|
||||
/** Marker interface for classes that represent commands */
|
||||
interface Command
|
||||
interface CommandData
|
||||
|
||||
/** Commands that inherit from this are intended to have no data items: it's only their presence that matters. */
|
||||
abstract class TypeOnlyCommand : Command {
|
||||
abstract class TypeOnlyCommandData : CommandData {
|
||||
override fun equals(other: Any?) = other?.javaClass == javaClass
|
||||
override fun hashCode() = javaClass.name.hashCode()
|
||||
}
|
||||
|
||||
/** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */
|
||||
data class Command(val data: CommandData, val pubkeys: List<PublicKey>) {
|
||||
constructor(data: CommandData, key: PublicKey) : this(data, listOf(key))
|
||||
}
|
||||
|
||||
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
|
||||
data class AuthenticatedObject<out T : Any>(
|
||||
val signers: List<PublicKey>,
|
||||
@ -79,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,14 @@
|
||||
|
||||
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.Clock
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
@ -19,26 +23,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.
|
||||
*
|
||||
@ -46,35 +47,57 @@ import java.util.*
|
||||
* database and replaced with the real objects. TFV is the form that is finally fed into the contracts.
|
||||
*/
|
||||
|
||||
/** Serialized command plus pubkey pair: the signature is stored at the end of the serialized bytes */
|
||||
data class WireCommand(val command: Command, val pubkeys: List<PublicKey>) {
|
||||
constructor(command: Command, key: PublicKey) : this(command, listOf(key))
|
||||
}
|
||||
|
||||
/** Transaction ready for serialisation, without any signatures attached. */
|
||||
data class WireTransaction(val inputStates: List<ContractStateRef>,
|
||||
val outputStates: List<ContractState>,
|
||||
val commands: List<WireCommand>) {
|
||||
fun toLedgerTransaction(timestamp: Instant?, identityService: IdentityService, originalHash: SecureHash): LedgerTransaction {
|
||||
val commands: List<Command>) {
|
||||
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.command)
|
||||
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 NotOnTimeException : 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<WireCommand> = 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 after building is
|
||||
* finished.
|
||||
*
|
||||
* The window of time in which the final timestamp may lie is defined as [time] +/- [timeTolerance].
|
||||
* If you want a non-symmetrical time window you must add the command via [addCommand] yourself. The tolerance
|
||||
* should be chosen such that your code can finish building the transaction and sending it to the TSA within that
|
||||
* window of time, taking into account factors such as network latency. Transactions being built by a group of
|
||||
* collaborating parties may therefore require a higher time tolerance than a transaction being built by a single
|
||||
* node.
|
||||
*/
|
||||
fun setTime(time: Instant, authenticatedBy: Party, timeTolerance: Duration) {
|
||||
check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" }
|
||||
commands.removeAll { it.data is TimestampCommand }
|
||||
addCommand(TimestampCommand(time, timeTolerance), 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) {
|
||||
when (t) {
|
||||
is ContractStateRef -> inputStates.add(t)
|
||||
is ContractState -> outputStates.add(t)
|
||||
is WireCommand -> commands.add(t)
|
||||
is Command -> commands.add(t)
|
||||
else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
|
||||
}
|
||||
}
|
||||
@ -88,16 +111,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, clock: Clock = Clock.systemUTC()) {
|
||||
// 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(clock.instant(), t.before) > maxExpectedLatency)
|
||||
throw NotOnTimeException()
|
||||
|
||||
// The timestamper may also throw NotOnTimeException 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))
|
||||
}
|
||||
@ -112,17 +164,20 @@ class PartialTransaction(private val inputStates: MutableList<ContractStateRef>
|
||||
outputStates.add(state)
|
||||
}
|
||||
|
||||
fun addArg(arg: WireCommand) {
|
||||
fun addCommand(arg: Command) {
|
||||
check(currentSigs.isEmpty())
|
||||
|
||||
// We should probably merge the lists of pubkeys for identical commands here.
|
||||
commands.add(arg)
|
||||
}
|
||||
|
||||
fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys)))
|
||||
fun addCommand(data: CommandData, keys: List<PublicKey>) = addCommand(Command(data, keys))
|
||||
|
||||
// Accessors that yield immutable snapshots.
|
||||
fun inputStates(): List<ContractStateRef> = ArrayList(inputStates)
|
||||
fun outputStates(): List<ContractState> = ArrayList(outputStates)
|
||||
fun commands(): List<WireCommand> = ArrayList(commands)
|
||||
fun commands(): List<Command> = ArrayList(commands)
|
||||
}
|
||||
|
||||
data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, val sigs: List<DigitalSignature.WithKey>) {
|
||||
@ -155,43 +210,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.
|
||||
* 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.
|
||||
*/
|
||||
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)
|
||||
fun verifyToLedgerTransaction(identityService: IdentityService): LedgerTransaction {
|
||||
return verify().toLedgerTransaction(identityService, txBits.bits.sha256())
|
||||
}
|
||||
}
|
||||
|
||||
@ -206,12 +235,9 @@ data class LedgerTransaction(
|
||||
/** The states that will be generated by the execution of this transaction. */
|
||||
val outStates: List<ContractState>,
|
||||
/** Arbitrary data passed to the program of each input state. */
|
||||
val commands: List<AuthenticatedObject<Command>>,
|
||||
/** The moment the transaction was timestamped for, if a timestamp was present. */
|
||||
val time: Instant?,
|
||||
val commands: List<AuthenticatedObject<CommandData>>,
|
||||
/** 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))
|
||||
@ -224,11 +250,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<Command>>,
|
||||
val time: Instant?,
|
||||
val commands: List<AuthenticatedObject<CommandData>>,
|
||||
val origHash: SecureHash) {
|
||||
override fun hashCode() = origHash.hashCode()
|
||||
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
|
||||
@ -258,6 +284,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)
|
||||
|
@ -110,7 +110,7 @@ class CashTests {
|
||||
assertEquals(100.DOLLARS, s.amount)
|
||||
assertEquals(MINI_CORP, s.deposit.party)
|
||||
assertEquals(DUMMY_PUBKEY_1, s.owner)
|
||||
assertTrue(ptx.commands()[0].command is Cash.Commands.Issue)
|
||||
assertTrue(ptx.commands()[0].data is Cash.Commands.Issue)
|
||||
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].pubkeys[0])
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,9 @@ package contracts
|
||||
import core.*
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@ -39,6 +41,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 +54,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,12 +67,35 @@ 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")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `timestamp out of range`() {
|
||||
// Check what happens if the timestamp on the transaction itself defines a range that doesn't include true
|
||||
// time as measured by a TSA (which is running five hours ahead in this test).
|
||||
CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply {
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
assertFailsWith(NotOnTimeException::class) {
|
||||
timestamp(DummyTimestamper(Clock.fixed(TEST_TX_TIME + 5.hours, ZoneOffset.UTC)))
|
||||
}
|
||||
}
|
||||
// Check that it also fails if true time is before the threshold (we are trying to timestamp too early).
|
||||
CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply {
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
assertFailsWith(NotOnTimeException::class) {
|
||||
val tsaClock = Clock.fixed(TEST_TX_TIME - 5.hours, ZoneOffset.UTC)
|
||||
timestamp(DummyTimestamper(tsaClock), Clock.fixed(TEST_TX_TIME, ZoneOffset.UTC))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue cannot replace an existing state`() {
|
||||
transactionGroup {
|
||||
@ -79,6 +106,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 +124,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 +132,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, 30.seconds)
|
||||
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 +155,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 +166,12 @@ class CommercialPaperTests {
|
||||
|
||||
fun makeRedeemTX(time: Instant): LedgerTransaction {
|
||||
val ptx = PartialTransaction()
|
||||
ptx.setTime(time, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
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)
|
||||
@ -167,6 +200,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 +216,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 +227,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, 30.seconds)
|
||||
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, 30.seconds)
|
||||
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, 30.seconds)
|
||||
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).
|
||||
@ -31,7 +30,7 @@ class TransactionSerializationTests {
|
||||
@Before
|
||||
fun setup() {
|
||||
tx = PartialTransaction().withItems(
|
||||
fakeStateRef, outputState, changeState, WireCommand(Cash.Commands.Move(), arrayListOf(TestUtils.keypair.public))
|
||||
fakeStateRef, outputState, changeState, Command(Cash.Commands.Move(), arrayListOf(TestUtils.keypair.public))
|
||||
)
|
||||
}
|
||||
|
||||
@ -77,7 +76,7 @@ class TransactionSerializationTests {
|
||||
// If the signature was replaced in transit, we don't like it.
|
||||
assertFailsWith(SignatureException::class) {
|
||||
val tx2 = PartialTransaction().withItems(fakeStateRef, outputState, changeState,
|
||||
WireCommand(Cash.Commands.Move(), arrayListOf(TestUtils.keypair2.public)))
|
||||
Command(Cash.Commands.Move(), TestUtils.keypair2.public))
|
||||
tx2.signWith(TestUtils.keypair2)
|
||||
|
||||
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify()
|
||||
@ -86,18 +85,14 @@ class TransactionSerializationTests {
|
||||
|
||||
@Test
|
||||
fun timestamp() {
|
||||
tx.setTime(TEST_TX_TIME, DUMMY_TIMESTAMPER.identity, 30.seconds)
|
||||
tx.timestamp(DUMMY_TIMESTAMPER)
|
||||
tx.signWith(TestUtils.keypair)
|
||||
val ttx = tx.toSignedTransaction().toTimestampedTransactionWithoutTime()
|
||||
val ltx = ttx.verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService)
|
||||
assertEquals(tx.commands().map { it.command }, ltx.commands.map { it.value })
|
||||
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 NotOnTimeException()
|
||||
return DummyTimestampingAuthority.key.signWithECDSA(wtxBytes.bits, identity)
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,13 +168,26 @@ infix fun ContractState.label(label: String) = LabeledOutput(label, this)
|
||||
|
||||
abstract class AbstractTransactionForTest {
|
||||
protected val outStates = ArrayList<LabeledOutput>()
|
||||
protected val commands = ArrayList<AuthenticatedObject<Command>>()
|
||||
protected val commands = ArrayList<Command>()
|
||||
|
||||
open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, s()).apply { outStates.add(this) }
|
||||
|
||||
fun arg(vararg key: PublicKey, c: () -> Command) {
|
||||
protected fun commandsToAuthenticatedObjects(): List<AuthenticatedObject<CommandData>> {
|
||||
return commands.map { AuthenticatedObject(it.pubkeys, it.pubkeys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, it.data) }
|
||||
}
|
||||
|
||||
fun arg(vararg key: PublicKey, c: () -> CommandData) {
|
||||
val keys = listOf(*key)
|
||||
commands.add(AuthenticatedObject(keys, keys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, c()))
|
||||
commands.add(Command(c(), keys))
|
||||
}
|
||||
|
||||
fun timestamp(time: Instant) {
|
||||
val data = TimestampCommand(time, 30.seconds)
|
||||
timestamp(data)
|
||||
}
|
||||
|
||||
fun timestamp(data: TimestampCommand) {
|
||||
commands.add(Command(data, DUMMY_TIMESTAMPER.identity.owningKey))
|
||||
}
|
||||
|
||||
// Forbid patterns like: transaction { ... transaction { ... } }
|
||||
@ -196,7 +201,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 }, commands, time, SecureHash.randomSHA256())
|
||||
val cmds = commandsToAuthenticatedObjects()
|
||||
val tx = TransactionForVerification(inStates, outStates.map { it.state }, cmds, SecureHash.randomSHA256())
|
||||
tx.verify(TEST_PROGRAM_MAP)
|
||||
}
|
||||
|
||||
@ -276,12 +282,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 {
|
||||
val wireCmds = commands.map { WireCommand(it.value, it.signers) }
|
||||
return WireTransaction(inStates, outStates.map { it.state }, wireCmds).toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256())
|
||||
fun toLedgerTransaction(): LedgerTransaction {
|
||||
val wtx = WireTransaction(inStates, outStates.map { it.state }, commands)
|
||||
return wtx.toLedgerTransaction(MockIdentityService, SecureHash.randomSHA256())
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,8 +297,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)
|
||||
@ -317,7 +323,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)
|
||||
@ -330,17 +336,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
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
package core.visualiser
|
||||
|
||||
import core.Command
|
||||
import core.CommandData
|
||||
import core.ContractState
|
||||
import core.SecureHash
|
||||
import core.testutils.TransactionGroupDSL
|
||||
@ -67,7 +67,7 @@ class GraphVisualiser(val dsl: TransactionGroupDSL<in ContractState>) {
|
||||
return dsl.labelForState(state) ?: stateToTypeName(state)
|
||||
}
|
||||
|
||||
private fun commandToTypeName(state: Command) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.')
|
||||
private fun commandToTypeName(state: CommandData) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.')
|
||||
private fun stateToTypeName(state: ContractState) = state.javaClass.canonicalName.removePrefix("contracts.").removeSuffix(".State")
|
||||
private fun stateToCSSClass(state: ContractState) = stateToTypeName(state).replace('.', '_').toLowerCase()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user