mirror of
https://github.com/corda/corda.git
synced 2024-12-20 13:33:12 +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
|
consume/destroy, these are called **inputs**, and contains a set of new states that it will create, these are called
|
||||||
**outputs**.
|
**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
|
**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
|
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.
|
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
|
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
|
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
|
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
|
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
|
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.
|
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,
|
Transactions may sometimes need to provide a contract with data from the outside world. Examples may include stock
|
||||||
their existence is given as input to the contract which can then decide which set of signatures it demands (if any).
|
prices, facts about events or the statuses of legal entities (e.g. bankruptcy), and so on. The providers of such
|
||||||
Signatures may be from an arbitrary, random **public key** that has no identity attached. A public key may be
|
facts are called **oracles** and they provide facts to the ledger by signing transactions that contain commands they
|
||||||
well known, that is, appears in some sort of public identity registry. In this case we say the key is owned by a
|
recognise. The commands contain the fact and the signature shows agreement to that fact. Time is also modelled as
|
||||||
**party**, which is defined (for now) as being merely a (public key, name) pair.
|
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
|
||||||
A transaction may also be **timestamped**. A timestamp is a (hash, datetime, signature) triple from a
|
time" (i.e. GPS time as calibrated to the US Naval Observatory).
|
||||||
*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.
|
|
||||||
|
|
||||||
As the same terminology often crops up in different distributed ledger designs, let's compare this to other
|
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
|
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
|
// Just for grouping
|
||||||
interface Commands : Command {
|
interface Commands : CommandData {
|
||||||
class Move() : TypeOnlyCommand(), Commands
|
class Move() : TypeOnlyCommandData(), Commands
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction
|
* 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.inputStates().isEmpty())
|
||||||
check(tx.outputStates().sumCashOrNull() == null)
|
check(tx.outputStates().sumCashOrNull() == null)
|
||||||
tx.addOutputState(Cash.State(at, amount, owner))
|
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)
|
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?
|
// What if we already have a move command with the right keys? Filter it out here or in platform code?
|
||||||
val keysList = keysUsed.toList()
|
val keysList = keysUsed.toList()
|
||||||
tx.addArg(WireCommand(Commands.Move(), keysList))
|
tx.addCommand(Commands.Move(), keysList)
|
||||||
return keysList
|
return keysList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,12 +53,12 @@ class CommercialPaper : Contract {
|
|||||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Commands : Command {
|
interface Commands : CommandData {
|
||||||
class Move : TypeOnlyCommand(), Commands
|
class Move : TypeOnlyCommandData(), Commands
|
||||||
class Redeem : TypeOnlyCommand(), 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.
|
// 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.
|
// 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) {
|
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
|
// 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.
|
// it for cash on or after the maturity date.
|
||||||
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
|
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
|
||||||
val time = tx.time
|
val timestamp: TimestampCommand? = tx.getTimestampBy(DummyTimestampingAuthority.identity)
|
||||||
|
|
||||||
for (group in groups) {
|
for (group in groups) {
|
||||||
when (command.value) {
|
when (command.value) {
|
||||||
@ -83,7 +83,7 @@ class CommercialPaper : Contract {
|
|||||||
is Commands.Redeem -> {
|
is Commands.Redeem -> {
|
||||||
val input = group.inputs.single()
|
val input = group.inputs.single()
|
||||||
val received = tx.outStates.sumCashBy(input.owner)
|
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 {
|
requireThat {
|
||||||
"the paper must have matured" by (time > input.maturityDate)
|
"the paper must have matured" by (time > input.maturityDate)
|
||||||
"the received amount equals the face value" by (received == input.faceValue)
|
"the received amount equals the face value" by (received == input.faceValue)
|
||||||
@ -94,7 +94,7 @@ class CommercialPaper : Contract {
|
|||||||
|
|
||||||
is Commands.Issue -> {
|
is Commands.Issue -> {
|
||||||
val output = group.outputs.single()
|
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 {
|
requireThat {
|
||||||
// Don't allow people to issue commercial paper under other entities identities.
|
// Don't allow people to issue commercial paper under other entities identities.
|
||||||
"the issuance is signed by the claimed issuer of the paper" by
|
"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 {
|
fun craftIssue(issuance: PartyReference, faceValue: Amount, maturityDate: Instant): PartialTransaction {
|
||||||
val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate)
|
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) {
|
fun craftMove(tx: PartialTransaction, paper: StateAndRef<State>, newOwner: PublicKey) {
|
||||||
tx.addInputState(paper.ref)
|
tx.addInputState(paper.ref)
|
||||||
tx.addOutputState(paper.state.copy(owner = newOwner))
|
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.
|
// Add the cash movement using the states in our wallet.
|
||||||
Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet)
|
Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet)
|
||||||
tx.addInputState(paper.ref)
|
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 {
|
interface Commands : CommandData {
|
||||||
class Register : TypeOnlyCommand(), Commands
|
class Register : TypeOnlyCommandData(), Commands
|
||||||
class Pledge : TypeOnlyCommand(), Commands
|
class Pledge : TypeOnlyCommandData(), Commands
|
||||||
class Close : TypeOnlyCommand(), Commands
|
class Close : TypeOnlyCommandData(), Commands
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun verify(tx: TransactionForVerification) {
|
override fun verify(tx: TransactionForVerification) {
|
||||||
@ -79,7 +79,9 @@ class CrowdFund : Contract {
|
|||||||
val command = tx.commands.requireSingleCommand<CrowdFund.Commands>()
|
val command = tx.commands.requireSingleCommand<CrowdFund.Commands>()
|
||||||
val outputCrowdFund: CrowdFund.State = tx.outStates.filterIsInstance<CrowdFund.State>().single()
|
val outputCrowdFund: CrowdFund.State = tx.outStates.filterIsInstance<CrowdFund.State>().single()
|
||||||
val outputCash: List<Cash.State> = tx.outStates.filterIsInstance<Cash.State>()
|
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) {
|
when (command.value) {
|
||||||
is Commands.Register -> {
|
is Commands.Register -> {
|
||||||
@ -89,7 +91,7 @@ class CrowdFund : Contract {
|
|||||||
"the output registration is empty of pledges" by (outputCrowdFund.pledges.isEmpty())
|
"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 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 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)
|
"the output registration has an open state" by (!outputCrowdFund.closed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,7 +99,6 @@ class CrowdFund : Contract {
|
|||||||
is Commands.Pledge -> {
|
is Commands.Pledge -> {
|
||||||
val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single()
|
val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single()
|
||||||
val pledgedCash = outputCash.sumCashBy(inputCrowdFund.campaign.owner)
|
val pledgedCash = outputCash.sumCashBy(inputCrowdFund.campaign.owner)
|
||||||
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign)
|
"campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign)
|
||||||
"the campaign is still open" by (inputCrowdFund.campaign.closingTime >= time)
|
"the campaign is still open" by (inputCrowdFund.campaign.closingTime >= time)
|
||||||
@ -117,8 +118,6 @@ class CrowdFund : Contract {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
|
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign)
|
"campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign)
|
||||||
"the closing date has past" by (time >= outputCrowdFund.campaign.closingTime)
|
"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 {
|
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 campaign = Campaign(owner = owner.party.owningKey, name = fundingName, target = fundingTarget, closingTime = closingTime)
|
||||||
val state = State(campaign)
|
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(
|
tx.addOutputState(campaign.state.copy(
|
||||||
pledges = campaign.state.pledges + CrowdFund.Pledge(subscriber, 1000.DOLLARS)
|
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>>) {
|
fun craftClose(tx: PartialTransaction, campaign: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) {
|
||||||
tx.addInputState(campaign.ref)
|
tx.addInputState(campaign.ref)
|
||||||
tx.addOutputState(campaign.state.copy(closed = true))
|
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 target has not been met, compose cash returns
|
||||||
if (campaign.state.pledgedAmount < campaign.state.campaign.target) {
|
if (campaign.state.pledgedAmount < campaign.state.campaign.target) {
|
||||||
for (pledge in campaign.state.pledges) {
|
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 {
|
public static class Move extends Commands {
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
@ -114,9 +114,12 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
List<InOutGroup<State>> groups = tx.groupStates(State.class, State::withoutOwner);
|
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.
|
// 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) {
|
for (InOutGroup<State> group : groups) {
|
||||||
List<State> inputs = group.getInputs();
|
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");
|
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) {
|
} else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) {
|
||||||
Amount received = CashKt.sumCashOrNull(inputs);
|
Amount received = CashKt.sumCashOrNull(inputs);
|
||||||
if (time == null)
|
|
||||||
throw new IllegalArgumentException("Redemption transactions must be timestamped");
|
|
||||||
if (received == null)
|
if (received == null)
|
||||||
throw new IllegalStateException("Failed requirement: no cash being redeemed");
|
throw new IllegalStateException("Failed requirement: no cash being redeemed");
|
||||||
if (input.getMaturityDate().isAfter(time))
|
if (input.getMaturityDate().isAfter(time))
|
||||||
|
@ -58,8 +58,8 @@ abstract class TwoPartyTradeProtocol {
|
|||||||
|
|
||||||
abstract fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer
|
abstract fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer
|
||||||
|
|
||||||
abstract class Buyer : ProtocolStateMachine<BuyerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
abstract class Buyer : ProtocolStateMachine<BuyerInitialArgs, Pair<WireTransaction, LedgerTransaction>>()
|
||||||
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
|
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<WireTransaction, LedgerTransaction>>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic fun create(smm: StateMachineManager): TwoPartyTradeProtocol {
|
@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
|
// 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.
|
// learn more about the protocol state machine framework.
|
||||||
class SellerImpl : Seller() {
|
class SellerImpl : Seller() {
|
||||||
override fun call(args: SellerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> {
|
override fun call(args: SellerInitialArgs): Pair<WireTransaction, LedgerTransaction> {
|
||||||
val sessionID = random63BitValue()
|
val sessionID = random63BitValue()
|
||||||
|
|
||||||
// Make the first message we'll send to kick off the protocol.
|
// 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" }
|
logger().trace { "Received partially signed transaction" }
|
||||||
|
|
||||||
partialTX.verifySignatures()
|
partialTX.verifySignatures()
|
||||||
val wtx = partialTX.txBits.deserialize<WireTransaction>()
|
val wtx: WireTransaction = partialTX.txBits.deserialize()
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(args.myKeyPair.public) == args.price)
|
"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.
|
// 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)
|
val fullySigned: SignedWireTransaction = partialTX.copy(sigs = partialTX.sigs + ourSignature)
|
||||||
// We should run it through our full TransactionGroup of all transactions here.
|
// We should run it through our full TransactionGroup of all transactions here.
|
||||||
fullySigned.verify()
|
fullySigned.verify()
|
||||||
val timestamped: TimestampedWireTransaction = fullySigned.toTimestampedTransaction(serviceHub.timestampingService)
|
|
||||||
logger().trace { "Built finished transaction, sending back to secondary!" }
|
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.
|
// The buyer's side of the protocol. See note above Seller to learn about the caveats here.
|
||||||
class BuyerImpl : Buyer() {
|
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.
|
// Wait for a trade request to come in on our pre-provided session ID.
|
||||||
val tradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, args.sessionID)
|
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 freshKey = serviceHub.keyManagementService.freshKey()
|
||||||
val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public)
|
val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public)
|
||||||
ptx.addOutputState(state)
|
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.
|
// Now sign the transaction with whatever keys we need to move the cash.
|
||||||
for (k in cashSigningPubKeys) {
|
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 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.
|
// TODO: Protect against a malicious buyer sending us back a different transaction to the one we built.
|
||||||
val fullySigned = sendAndReceive<TimestampedWireTransaction>(TRADE_TOPIC,
|
val fullySigned = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, tradeRequest.sessionID, args.sessionID, stx)
|
||||||
tradeRequest.sessionID, args.sessionID, stx)
|
|
||||||
|
|
||||||
logger().trace { "Got fully signed transaction, verifying ... "}
|
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! :-)" }
|
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 ///////////////////////////////////////////////////////////////////////////////////////////
|
//// Authenticated commands ///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
/** Filters the command list by type, party and public key all at once. */
|
/** 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 { it.value is T }.
|
||||||
filter { if (signer == null) true else it.signers.contains(signer) }.
|
filter { if (signer == null) true else it.signers.contains(signer) }.
|
||||||
filter { if (party == null) true else it.signingParties.contains(party) }.
|
filter { if (party == null) true else it.signingParties.contains(party) }.
|
||||||
map { AuthenticatedObject<T>(it.signers, it.signingParties, it.value as T) }
|
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()
|
select<T>().single()
|
||||||
} catch (e: NoSuchElementException) {
|
} catch (e: NoSuchElementException) {
|
||||||
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
|
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
|
||||||
}
|
}
|
||||||
|
|
||||||
// For Java
|
// 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)
|
class LegallyIdentifiable(val signer: Party, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||||
@ -90,8 +89,16 @@ fun PrivateKey.signWithECDSA(bits: ByteArray): DigitalSignature {
|
|||||||
return DigitalSignature(sig)
|
return DigitalSignature(sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PrivateKey.signWithECDSA(bits: ByteArray, publicKey: PublicKey) = DigitalSignature.WithKey(publicKey, signWithECDSA(bits).bits)
|
fun PrivateKey.signWithECDSA(bitsToSign: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey {
|
||||||
fun KeyPair.signWithECDSA(bits: ByteArray) = private.signWithECDSA(bits, public)
|
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 */
|
/** Utility to simplify the act of verifying a signature */
|
||||||
fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) {
|
fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) {
|
||||||
|
@ -9,10 +9,11 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import core.messaging.MessagingService
|
import core.messaging.MessagingService
|
||||||
|
import core.serialization.SerializedBytes
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.PublicKey
|
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
|
* 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
|
* Simple interface (for testing) to an abstract timestamping service. Note that this is not "timestamping" in the
|
||||||
* 'timestamping' in the block chain sense, but rather, implies a semi-trusted third party taking a reading of the
|
* blockchain sense of a total ordering of transactions, but rather, a signature from a well known/trusted timestamping
|
||||||
* current time, typically from an atomic clock, and then digitally signing (current time, hash) to produce a timestamp
|
* service over a transaction that indicates the timestamp in it is accurate. Such a signature may not always be
|
||||||
* triple (signature, time, hash). The purpose of these timestamps is to locate a transaction in the timeline, which is
|
* necessary: if there are multiple parties involved in a transaction then they can cross-check the timestamp
|
||||||
* important in the absence of blocks. Here we model the timestamp as an opaque byte array.
|
* themselves.
|
||||||
*/
|
*/
|
||||||
interface TimestamperService {
|
interface TimestamperService {
|
||||||
fun timestamp(hash: SecureHash): ByteArray
|
fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable
|
||||||
fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant
|
|
||||||
|
/** 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.OpaqueBytes
|
||||||
import core.serialization.serialize
|
import core.serialization.serialize
|
||||||
import java.security.PublicKey
|
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
|
* 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
|
val owner: PublicKey
|
||||||
|
|
||||||
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
|
/** 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!) */
|
/** 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 */
|
/** 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. */
|
/** 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 equals(other: Any?) = other?.javaClass == javaClass
|
||||||
override fun hashCode() = javaClass.name.hashCode()
|
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. */
|
/** 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>(
|
data class AuthenticatedObject<out T : Any>(
|
||||||
val signers: List<PublicKey>,
|
val signers: List<PublicKey>,
|
||||||
@ -79,6 +86,23 @@ data class AuthenticatedObject<out T : Any>(
|
|||||||
val value: T
|
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
|
* 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
|
* 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.
|
// Look up the output in that transaction by index.
|
||||||
inputs.add(ltx.outStates[ref.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)
|
for (tx in resolved)
|
||||||
|
@ -8,10 +8,14 @@
|
|||||||
|
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import core.serialization.*
|
import core.serialization.SerializedBytes
|
||||||
|
import core.serialization.deserialize
|
||||||
|
import core.serialization.serialize
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
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
|
* Views of a transaction as it progresses through the pipeline, from bytes loaded from disk/network to the object
|
||||||
* tree passed into a contract.
|
* 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
|
* 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.
|
* 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
|
* 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
|
* 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
|
* keypairs. Note that a sighash is not the same thing as a *transaction id*, which is the hash of a SignedWireTransaction
|
||||||
* TimestampedWireTransaction i.e. the outermost serialised form with everything included.
|
* 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
|
* 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.
|
* 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
|
* Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from
|
||||||
* multiple parties.
|
* multiple parties.
|
||||||
*
|
*
|
||||||
* LedgerTransaction is derived from WireTransaction and TimestampedWireTransaction together. It is the result of
|
* LedgerTransaction is derived from WireTransaction. It is the result of doing some basic key lookups on WireCommand
|
||||||
* doing some basic key lookups on WireCommand to see if any keys are from a recognised party, thus converting
|
* to see if any keys are from a recognised party, thus converting the WireCommand objects into
|
||||||
* the WireCommand objects into AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party
|
* AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party map. In future it'd make more
|
||||||
* map. In future it'd make more sense to use a certificate scheme and so that logic would get more complex.
|
* 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.
|
* 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.
|
* 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. */
|
/** Transaction ready for serialisation, without any signatures attached. */
|
||||||
data class WireTransaction(val inputStates: List<ContractStateRef>,
|
data class WireTransaction(val inputStates: List<ContractStateRef>,
|
||||||
val outputStates: List<ContractState>,
|
val outputStates: List<ContractState>,
|
||||||
val commands: List<WireCommand>) {
|
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 authenticatedArgs = commands.map {
|
||||||
val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) }
|
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. */
|
/** A mutable transaction that's in the process of being built, before all signatures are present. */
|
||||||
class PartialTransaction(private val inputStates: MutableList<ContractStateRef> = arrayListOf(),
|
class PartialTransaction(private val inputStates: MutableList<ContractStateRef> = arrayListOf(),
|
||||||
private val outputStates: MutableList<ContractState> = 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 */
|
/** 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 {
|
public fun withItems(vararg items: Any): PartialTransaction {
|
||||||
for (t in items) {
|
for (t in items) {
|
||||||
when (t) {
|
when (t) {
|
||||||
is ContractStateRef -> inputStates.add(t)
|
is ContractStateRef -> inputStates.add(t)
|
||||||
is ContractState -> outputStates.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}")
|
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(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" }
|
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()
|
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 {
|
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedWireTransaction {
|
||||||
if (checkSufficientSignatures) {
|
if (checkSufficientSignatures) {
|
||||||
val requiredKeys = commands.flatMap { it.pubkeys }.toSet()
|
|
||||||
val gotKeys = currentSigs.map { it.by }.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))
|
return SignedWireTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
|
||||||
}
|
}
|
||||||
@ -112,17 +164,20 @@ class PartialTransaction(private val inputStates: MutableList<ContractStateRef>
|
|||||||
outputStates.add(state)
|
outputStates.add(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addArg(arg: WireCommand) {
|
fun addCommand(arg: Command) {
|
||||||
check(currentSigs.isEmpty())
|
check(currentSigs.isEmpty())
|
||||||
|
|
||||||
// We should probably merge the lists of pubkeys for identical commands here.
|
// We should probably merge the lists of pubkeys for identical commands here.
|
||||||
commands.add(arg)
|
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.
|
// Accessors that yield immutable snapshots.
|
||||||
fun inputStates(): List<ContractStateRef> = ArrayList(inputStates)
|
fun inputStates(): List<ContractStateRef> = ArrayList(inputStates)
|
||||||
fun outputStates(): List<ContractState> = ArrayList(outputStates)
|
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>) {
|
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.
|
// unverified.
|
||||||
val cmdKeys = wtx.commands.flatMap { it.pubkeys }.toSet()
|
val cmdKeys = wtx.commands.flatMap { it.pubkeys }.toSet()
|
||||||
val sigKeys = sigs.map { it.by }.toSet()
|
val sigKeys = sigs.map { it.by }.toSet()
|
||||||
if (cmdKeys != sigKeys)
|
if (!sigKeys.containsAll(cmdKeys))
|
||||||
throw SignatureException("Command keys don't match the signatures: $cmdKeys vs $sigKeys")
|
throw SignatureException("Missing signatures on the transaction for: ${cmdKeys - sigKeys}")
|
||||||
return wtx
|
return wtx
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Uses the given timestamper service to calculate a signed timestamp and then returns a wrapper for both */
|
/**
|
||||||
fun toTimestampedTransaction(timestamper: TimestamperService): TimestampedWireTransaction {
|
* Calls [verify] to check all required signatures are present, and then uses the passed [IdentityService] to call
|
||||||
val bits = serialize()
|
* [WireTransaction.toLedgerTransaction] to look up well known identities from pubkeys.
|
||||||
return TimestampedWireTransaction(bits, timestamper.timestamp(bits.sha256()).opaque())
|
*/
|
||||||
}
|
fun verifyToLedgerTransaction(identityService: IdentityService): LedgerTransaction {
|
||||||
|
return verify().toLedgerTransaction(identityService, txBits.bits.sha256())
|
||||||
/** 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,12 +235,9 @@ data class LedgerTransaction(
|
|||||||
/** The states that will be generated by the execution of this transaction. */
|
/** The states that will be generated by the execution of this transaction. */
|
||||||
val outStates: List<ContractState>,
|
val outStates: List<ContractState>,
|
||||||
/** Arbitrary data passed to the program of each input state. */
|
/** Arbitrary data passed to the program of each input state. */
|
||||||
val commands: List<AuthenticatedObject<Command>>,
|
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 */
|
/** The hash of the original serialised TimestampedWireTransaction or SignedTransaction */
|
||||||
val hash: SecureHash
|
val hash: SecureHash
|
||||||
// TODO: nLockTime equivalent?
|
|
||||||
) {
|
) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T : ContractState> outRef(index: Int) = StateAndRef(outStates[index] as T, ContractStateRef(hash, index))
|
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. */
|
/** 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>,
|
data class TransactionForVerification(val inStates: List<ContractState>,
|
||||||
val outStates: List<ContractState>,
|
val outStates: List<ContractState>,
|
||||||
val commands: List<AuthenticatedObject<Command>>,
|
val commands: List<AuthenticatedObject<CommandData>>,
|
||||||
val time: Instant?,
|
|
||||||
val origHash: SecureHash) {
|
val origHash: SecureHash) {
|
||||||
override fun hashCode() = origHash.hashCode()
|
override fun hashCode() = origHash.hashCode()
|
||||||
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
|
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>)
|
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.
|
// For Java users.
|
||||||
fun <T : ContractState> groupStates(ofType: Class<T>, selector: (T) -> Any): List<InOutGroup<T>> {
|
fun <T : ContractState> groupStates(ofType: Class<T>, selector: (T) -> Any): List<InOutGroup<T>> {
|
||||||
val inputs = inStates.filterIsInstance(ofType)
|
val inputs = inStates.filterIsInstance(ofType)
|
||||||
|
@ -110,7 +110,7 @@ class CashTests {
|
|||||||
assertEquals(100.DOLLARS, s.amount)
|
assertEquals(100.DOLLARS, s.amount)
|
||||||
assertEquals(MINI_CORP, s.deposit.party)
|
assertEquals(MINI_CORP, s.deposit.party)
|
||||||
assertEquals(DUMMY_PUBKEY_1, s.owner)
|
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])
|
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].pubkeys[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,9 @@ package contracts
|
|||||||
import core.*
|
import core.*
|
||||||
import core.testutils.*
|
import core.testutils.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.time.Clock
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.ZoneOffset
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
@ -39,6 +41,7 @@ class CommercialPaperTests {
|
|||||||
transaction {
|
transaction {
|
||||||
output { PAPER_1 }
|
output { PAPER_1 }
|
||||||
arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() }
|
arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() }
|
||||||
|
timestamp(TEST_TX_TIME)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectFailureOfTx(1, "signed by the claimed issuer")
|
expectFailureOfTx(1, "signed by the claimed issuer")
|
||||||
@ -51,6 +54,7 @@ class CommercialPaperTests {
|
|||||||
transaction {
|
transaction {
|
||||||
output { PAPER_1.copy(faceValue = 0.DOLLARS) }
|
output { PAPER_1.copy(faceValue = 0.DOLLARS) }
|
||||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||||
|
timestamp(TEST_TX_TIME)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectFailureOfTx(1, "face value is not zero")
|
expectFailureOfTx(1, "face value is not zero")
|
||||||
@ -63,12 +67,35 @@ class CommercialPaperTests {
|
|||||||
transaction {
|
transaction {
|
||||||
output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) }
|
output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) }
|
||||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||||
|
timestamp(TEST_TX_TIME)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectFailureOfTx(1, "maturity date is not in the past")
|
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
|
@Test
|
||||||
fun `issue cannot replace an existing state`() {
|
fun `issue cannot replace an existing state`() {
|
||||||
transactionGroup {
|
transactionGroup {
|
||||||
@ -79,6 +106,7 @@ class CommercialPaperTests {
|
|||||||
input("paper")
|
input("paper")
|
||||||
output { PAPER_1 }
|
output { PAPER_1 }
|
||||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||||
|
timestamp(TEST_TX_TIME)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectFailureOfTx(1, "there is no input state")
|
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>>> {
|
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)) })
|
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`() {
|
fun `issue move and then redeem`() {
|
||||||
// MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
|
// MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
|
||||||
val issueTX: LedgerTransaction = run {
|
val issueTX: LedgerTransaction = run {
|
||||||
val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days)
|
val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply {
|
||||||
ptx.signWith(MINI_CORP_KEY)
|
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||||
|
signWith(MINI_CORP_KEY)
|
||||||
|
timestamp(DUMMY_TIMESTAMPER)
|
||||||
|
}
|
||||||
val stx = ptx.toSignedTransaction()
|
val stx = ptx.toSignedTransaction()
|
||||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256())
|
stx.verifyToLedgerTransaction(MockIdentityService)
|
||||||
}
|
}
|
||||||
|
|
||||||
val (alicesWalletTX, alicesWallet) = cashOutputsToWallet(
|
val (alicesWalletTX, alicesWallet) = cashOutputsToWallet(
|
||||||
@ -124,7 +155,7 @@ class CommercialPaperTests {
|
|||||||
ptx.signWith(MINI_CORP_KEY)
|
ptx.signWith(MINI_CORP_KEY)
|
||||||
ptx.signWith(ALICE_KEY)
|
ptx.signWith(ALICE_KEY)
|
||||||
val stx = ptx.toSignedTransaction()
|
val stx = ptx.toSignedTransaction()
|
||||||
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256())
|
stx.verifyToLedgerTransaction(MockIdentityService)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Won't be validated.
|
// Won't be validated.
|
||||||
@ -135,10 +166,12 @@ class CommercialPaperTests {
|
|||||||
|
|
||||||
fun makeRedeemTX(time: Instant): LedgerTransaction {
|
fun makeRedeemTX(time: Instant): LedgerTransaction {
|
||||||
val ptx = PartialTransaction()
|
val ptx = PartialTransaction()
|
||||||
|
ptx.setTime(time, DummyTimestampingAuthority.identity, 30.seconds)
|
||||||
CommercialPaper().craftRedeem(ptx, moveTX.outRef(1), corpWallet)
|
CommercialPaper().craftRedeem(ptx, moveTX.outRef(1), corpWallet)
|
||||||
ptx.signWith(ALICE_KEY)
|
ptx.signWith(ALICE_KEY)
|
||||||
ptx.signWith(MINI_CORP_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)
|
val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)
|
||||||
@ -154,8 +187,8 @@ class CommercialPaperTests {
|
|||||||
|
|
||||||
// Generate a trade lifecycle with various parameters.
|
// Generate a trade lifecycle with various parameters.
|
||||||
fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
|
fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
|
||||||
aliceGetsBack: Amount = 1000.DOLLARS,
|
aliceGetsBack: Amount = 1000.DOLLARS,
|
||||||
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<CommercialPaper.State> {
|
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<CommercialPaper.State> {
|
||||||
val someProfits = 1200.DOLLARS
|
val someProfits = 1200.DOLLARS
|
||||||
return transactionGroupFor() {
|
return transactionGroupFor() {
|
||||||
roots {
|
roots {
|
||||||
@ -167,6 +200,7 @@ class CommercialPaperTests {
|
|||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
output("paper") { PAPER_1 }
|
output("paper") { PAPER_1 }
|
||||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
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,
|
// 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
|
// 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.
|
// 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("alice's paper")
|
||||||
input("some profits")
|
input("some profits")
|
||||||
|
|
||||||
@ -193,6 +227,8 @@ class CommercialPaperTests {
|
|||||||
|
|
||||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||||
arg(ALICE) { CommercialPaper.Commands.Redeem() }
|
arg(ALICE) { CommercialPaper.Commands.Redeem() }
|
||||||
|
|
||||||
|
timestamp(redemptionTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ class CrowdFundTests {
|
|||||||
transaction {
|
transaction {
|
||||||
output { CF_1 }
|
output { CF_1 }
|
||||||
arg(DUMMY_PUBKEY_1) { CrowdFund.Commands.Register() }
|
arg(DUMMY_PUBKEY_1) { CrowdFund.Commands.Register() }
|
||||||
|
timestamp(TEST_TX_TIME)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectFailureOfTx(1, "the transaction is signed by the owner of the crowdsourcing")
|
expectFailureOfTx(1, "the transaction is signed by the owner of the crowdsourcing")
|
||||||
@ -46,6 +47,7 @@ class CrowdFundTests {
|
|||||||
transaction {
|
transaction {
|
||||||
output { CF_1.copy(campaign = CF_1.campaign.copy(closingTime = TEST_TX_TIME - 1.days)) }
|
output { CF_1.copy(campaign = CF_1.campaign.copy(closingTime = TEST_TX_TIME - 1.days)) }
|
||||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
|
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
|
||||||
|
timestamp(TEST_TX_TIME)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectFailureOfTx(1, "the output registration has a closing time in the future")
|
expectFailureOfTx(1, "the output registration has a closing time in the future")
|
||||||
@ -67,6 +69,7 @@ class CrowdFundTests {
|
|||||||
transaction {
|
transaction {
|
||||||
output("funding opportunity") { CF_1 }
|
output("funding opportunity") { CF_1 }
|
||||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
|
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
|
||||||
|
timestamp(TEST_TX_TIME)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Place a pledge
|
// 2. Place a pledge
|
||||||
@ -81,19 +84,21 @@ class CrowdFundTests {
|
|||||||
output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY }
|
output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY }
|
||||||
arg(ALICE) { Cash.Commands.Move() }
|
arg(ALICE) { Cash.Commands.Move() }
|
||||||
arg(ALICE) { CrowdFund.Commands.Pledge() }
|
arg(ALICE) { CrowdFund.Commands.Pledge() }
|
||||||
|
timestamp(TEST_TX_TIME)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Close the opportunity, assuming the target has been met
|
// 3. Close the opportunity, assuming the target has been met
|
||||||
transaction(time = TEST_TX_TIME + 8.days) {
|
transaction {
|
||||||
input ("pledged opportunity")
|
input ("pledged opportunity")
|
||||||
output ("funded and closed") { "pledged opportunity".output.copy(closed = true) }
|
output ("funded and closed") { "pledged opportunity".output.copy(closed = true) }
|
||||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Close() }
|
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>>> {
|
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)) })
|
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.
|
// MiniCorp registers a crowdfunding of $1,000, to close in 7 days.
|
||||||
val registerTX: LedgerTransaction = run {
|
val registerTX: LedgerTransaction = run {
|
||||||
// craftRegister returns a partial transaction
|
// craftRegister returns a partial transaction
|
||||||
val ptx = CrowdFund().craftRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days)
|
val ptx = CrowdFund().craftRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days).apply {
|
||||||
ptx.signWith(MINI_CORP_KEY)
|
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||||
|
signWith(MINI_CORP_KEY)
|
||||||
|
timestamp(DUMMY_TIMESTAMPER)
|
||||||
|
}
|
||||||
val stx = ptx.toSignedTransaction()
|
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
|
// let's give Alice some funds that she can invest
|
||||||
@ -120,10 +128,12 @@ class CrowdFundTests {
|
|||||||
val ptx = PartialTransaction()
|
val ptx = PartialTransaction()
|
||||||
CrowdFund().craftPledge(ptx, registerTX.outRef(0), ALICE)
|
CrowdFund().craftPledge(ptx, registerTX.outRef(0), ALICE)
|
||||||
Cash().craftSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet)
|
Cash().craftSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet)
|
||||||
|
ptx.setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||||
ptx.signWith(ALICE_KEY)
|
ptx.signWith(ALICE_KEY)
|
||||||
|
ptx.timestamp(DUMMY_TIMESTAMPER)
|
||||||
val stx = ptx.toSignedTransaction()
|
val stx = ptx.toSignedTransaction()
|
||||||
// this verify passes - the transaction contains an output cash, necessary to verify the fund command
|
// 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.
|
// Won't be validated.
|
||||||
@ -134,10 +144,12 @@ class CrowdFundTests {
|
|||||||
// MiniCorp closes their campaign.
|
// MiniCorp closes their campaign.
|
||||||
fun makeFundedTX(time: Instant): LedgerTransaction {
|
fun makeFundedTX(time: Instant): LedgerTransaction {
|
||||||
val ptx = PartialTransaction()
|
val ptx = PartialTransaction()
|
||||||
|
ptx.setTime(time, DUMMY_TIMESTAMPER.identity, 30.seconds)
|
||||||
CrowdFund().craftClose(ptx, pledgeTX.outRef(0), miniCorpWallet)
|
CrowdFund().craftClose(ptx, pledgeTX.outRef(0), miniCorpWallet)
|
||||||
ptx.signWith(MINI_CORP_KEY)
|
ptx.signWith(MINI_CORP_KEY)
|
||||||
|
ptx.timestamp(DUMMY_TIMESTAMPER)
|
||||||
val stx = ptx.toSignedTransaction()
|
val stx = ptx.toSignedTransaction()
|
||||||
return stx.verify().toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256())
|
return stx.verifyToLedgerTransaction(MockIdentityService)
|
||||||
}
|
}
|
||||||
|
|
||||||
val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days)
|
val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days)
|
||||||
@ -150,7 +162,5 @@ class CrowdFundTests {
|
|||||||
|
|
||||||
// This verification passes
|
// This verification passes
|
||||||
TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify(TEST_PROGRAM_MAP)
|
TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify(TEST_PROGRAM_MAP)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -97,7 +97,7 @@ class TransactionGroupTests {
|
|||||||
// points nowhere.
|
// points nowhere.
|
||||||
val ref = ContractStateRef(SecureHash.randomSHA256(), 0)
|
val ref = ContractStateRef(SecureHash.randomSHA256(), 0)
|
||||||
tg.txns.add(LedgerTransaction(
|
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) {
|
val e = assertFailsWith(TransactionResolutionException::class) {
|
||||||
|
@ -16,7 +16,6 @@ import org.junit.Test
|
|||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
import kotlin.test.assertNull
|
|
||||||
|
|
||||||
class TransactionSerializationTests {
|
class TransactionSerializationTests {
|
||||||
// Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change).
|
// Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change).
|
||||||
@ -31,7 +30,7 @@ class TransactionSerializationTests {
|
|||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
tx = PartialTransaction().withItems(
|
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.
|
// If the signature was replaced in transit, we don't like it.
|
||||||
assertFailsWith(SignatureException::class) {
|
assertFailsWith(SignatureException::class) {
|
||||||
val tx2 = PartialTransaction().withItems(fakeStateRef, outputState, changeState,
|
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)
|
tx2.signWith(TestUtils.keypair2)
|
||||||
|
|
||||||
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify()
|
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify()
|
||||||
@ -86,18 +85,14 @@ class TransactionSerializationTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun timestamp() {
|
fun timestamp() {
|
||||||
|
tx.setTime(TEST_TX_TIME, DUMMY_TIMESTAMPER.identity, 30.seconds)
|
||||||
|
tx.timestamp(DUMMY_TIMESTAMPER)
|
||||||
tx.signWith(TestUtils.keypair)
|
tx.signWith(TestUtils.keypair)
|
||||||
val ttx = tx.toSignedTransaction().toTimestampedTransactionWithoutTime()
|
val stx = tx.toSignedTransaction()
|
||||||
val ltx = ttx.verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService)
|
val ltx = stx.verifyToLedgerTransaction(MockIdentityService)
|
||||||
assertEquals(tx.commands().map { it.command }, ltx.commands.map { it.value })
|
assertEquals(tx.commands().map { it.data }, ltx.commands.map { it.value })
|
||||||
assertEquals(tx.inputStates(), ltx.inStateRefs)
|
assertEquals(tx.inputStates(), ltx.inStateRefs)
|
||||||
assertEquals(tx.outputStates(), ltx.outStates)
|
assertEquals(tx.outputStates(), ltx.outStates)
|
||||||
assertNull(ltx.time)
|
assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_TIMESTAMPER.identity)!!.midpoint)
|
||||||
|
|
||||||
val ltx2: LedgerTransaction = tx.
|
|
||||||
toSignedTransaction().
|
|
||||||
toTimestampedTransaction(DUMMY_TIMESTAMPER).
|
|
||||||
verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService)
|
|
||||||
assertEquals(TEST_TX_TIME, ltx2.time)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,20 +10,20 @@
|
|||||||
|
|
||||||
package core.testutils
|
package core.testutils
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding
|
|
||||||
import contracts.*
|
import contracts.*
|
||||||
import core.*
|
import core.*
|
||||||
import core.messaging.MessagingService
|
import core.messaging.MessagingService
|
||||||
|
import core.serialization.SerializedBytes
|
||||||
|
import core.serialization.deserialize
|
||||||
import core.visualiser.GraphVisualiser
|
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.KeyPair
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.annotation.concurrent.ThreadSafe
|
import javax.annotation.concurrent.ThreadSafe
|
||||||
import kotlin.test.assertEquals
|
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
|
* A test/mock timestamping service that doesn't use any signatures or security. It timestamps with
|
||||||
* [TEST_TX_TIME], an arbitrary point on the timeline.
|
* 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 {
|
class DummyTimestamper(var clock: Clock = Clock.fixed(TEST_TX_TIME, ZoneId.systemDefault()),
|
||||||
override fun timestamp(hash: SecureHash): ByteArray {
|
val tolerance: Duration = 30.seconds) : TimestamperService {
|
||||||
val bos = ByteArrayOutputStream()
|
override val identity = DummyTimestampingAuthority.identity
|
||||||
DataOutputStream(bos).use {
|
|
||||||
it.writeLong(time.toEpochMilli())
|
|
||||||
it.write(hash.bits)
|
|
||||||
}
|
|
||||||
return bos.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant {
|
override fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable {
|
||||||
val dis = DataInputStream(ByteArrayInputStream(signedTimestamp))
|
val wtx = wtxBytes.deserialize()
|
||||||
val epochMillis = dis.readLong()
|
val timestamp = wtx.commands.mapNotNull { it.data as? TimestampCommand }.single()
|
||||||
val serHash = ByteArray(32)
|
if (Duration.between(timestamp.before, clock.instant()) > tolerance)
|
||||||
dis.readFully(serHash)
|
throw NotOnTimeException()
|
||||||
if (!Arrays.equals(serHash, hash.bits))
|
return DummyTimestampingAuthority.key.signWithECDSA(wtxBytes.bits, identity)
|
||||||
throw IllegalStateException("Hash mismatch: ${BaseEncoding.base16().encode(serHash)} vs ${BaseEncoding.base16().encode(hash.bits)}")
|
|
||||||
return Instant.ofEpochMilli(epochMillis)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,13 +168,26 @@ infix fun ContractState.label(label: String) = LabeledOutput(label, this)
|
|||||||
|
|
||||||
abstract class AbstractTransactionForTest {
|
abstract class AbstractTransactionForTest {
|
||||||
protected val outStates = ArrayList<LabeledOutput>()
|
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) }
|
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)
|
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 { ... } }
|
// Forbid patterns like: transaction { ... transaction { ... } }
|
||||||
@ -196,7 +201,8 @@ open class TransactionForTest : AbstractTransactionForTest() {
|
|||||||
fun input(s: () -> ContractState) = inStates.add(s())
|
fun input(s: () -> ContractState) = inStates.add(s())
|
||||||
|
|
||||||
protected fun run(time: Instant) {
|
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)
|
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
|
* Converts to a [LedgerTransaction] with the test institution map, and just assigns a random hash
|
||||||
* hash (i.e. pretend it was signed)
|
* (i.e. pretend it was signed)
|
||||||
*/
|
*/
|
||||||
fun toLedgerTransaction(time: Instant): LedgerTransaction {
|
fun toLedgerTransaction(): LedgerTransaction {
|
||||||
val wireCmds = commands.map { WireCommand(it.value, it.signers) }
|
val wtx = WireTransaction(inStates, outStates.map { it.state }, commands)
|
||||||
return WireTransaction(inStates, outStates.map { it.state }, wireCmds).toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256())
|
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)
|
fun <C : ContractState> lookup(label: String) = StateAndRef(label.output as C, label.outputRef)
|
||||||
|
|
||||||
private inner class InternalLedgerTransactionDSL : LedgerTransactionDSL() {
|
private inner class InternalLedgerTransactionDSL : LedgerTransactionDSL() {
|
||||||
fun finaliseAndInsertLabels(time: Instant): LedgerTransaction {
|
fun finaliseAndInsertLabels(): LedgerTransaction {
|
||||||
val ltx = toLedgerTransaction(time)
|
val ltx = toLedgerTransaction()
|
||||||
for ((index, labelledState) in outStates.withIndex()) {
|
for ((index, labelledState) in outStates.withIndex()) {
|
||||||
if (labelledState.label != null) {
|
if (labelledState.label != null) {
|
||||||
labelToRefs[labelledState.label] = ContractStateRef(ltx.hash, index)
|
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) {
|
fun transaction(vararg outputStates: LabeledOutput) {
|
||||||
val outs = outputStates.map { it.state }
|
val outs = outputStates.map { it.state }
|
||||||
val wtx = WireTransaction(emptyList(), outs, emptyList())
|
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()) {
|
for ((index, state) in outputStates.withIndex()) {
|
||||||
val label = state.label!!
|
val label = state.label!!
|
||||||
labelToRefs[label] = ContractStateRef(ltx.hash, index)
|
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)
|
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
||||||
fun roots(body: Roots.() -> Unit) {}
|
fun roots(body: Roots.() -> Unit) {}
|
||||||
@Deprecated("Use the vararg form of transaction inside roots", level = DeprecationLevel.ERROR)
|
@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() }
|
fun roots(body: Roots.() -> Unit) = Roots().apply { body() }
|
||||||
|
|
||||||
val txns = ArrayList<LedgerTransaction>()
|
val txns = ArrayList<LedgerTransaction>()
|
||||||
private val txnToLabelMap = HashMap<LedgerTransaction, String>()
|
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()
|
val forTest = InternalLedgerTransactionDSL()
|
||||||
forTest.body()
|
forTest.body()
|
||||||
val ltx = forTest.finaliseAndInsertLabels(time)
|
val ltx = forTest.finaliseAndInsertLabels()
|
||||||
txns.add(ltx)
|
txns.add(ltx)
|
||||||
if (label != null)
|
if (label != null)
|
||||||
txnToLabelMap[ltx] = label
|
txnToLabelMap[ltx] = label
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
package core.visualiser
|
package core.visualiser
|
||||||
|
|
||||||
import core.Command
|
import core.CommandData
|
||||||
import core.ContractState
|
import core.ContractState
|
||||||
import core.SecureHash
|
import core.SecureHash
|
||||||
import core.testutils.TransactionGroupDSL
|
import core.testutils.TransactionGroupDSL
|
||||||
@ -67,7 +67,7 @@ class GraphVisualiser(val dsl: TransactionGroupDSL<in ContractState>) {
|
|||||||
return dsl.labelForState(state) ?: stateToTypeName(state)
|
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 stateToTypeName(state: ContractState) = state.javaClass.canonicalName.removePrefix("contracts.").removeSuffix(".State")
|
||||||
private fun stateToCSSClass(state: ContractState) = stateToTypeName(state).replace('.', '_').toLowerCase()
|
private fun stateToCSSClass(state: ContractState) = stateToTypeName(state).replace('.', '_').toLowerCase()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user