mirror of
https://github.com/corda/corda.git
synced 2025-06-18 15:18:16 +00:00
Add a unit test to check before/after processing by the dummy timestamper.
Tweak the timestamping API a bit.
This commit is contained in:
@ -14,6 +14,7 @@ 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.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -63,7 +64,7 @@ data class WireTransaction(val inputStates: List<ContractStateRef>,
|
|||||||
* Thrown if an attempt is made to timestamp a transaction using a trusted timestamper, but the time on the transaction
|
* 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.
|
* is too far in the past or future relative to the local clock and thus the timestamper would reject it.
|
||||||
*/
|
*/
|
||||||
class TooLateException : Exception()
|
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(),
|
||||||
@ -74,12 +75,20 @@ class PartialTransaction(private val inputStates: MutableList<ContractStateRef>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Places a [TimestampCommand] in this transaction, removing any existing command if there is one.
|
* 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.
|
* 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) {
|
fun setTime(time: Instant, authenticatedBy: Party, timeTolerance: Duration) {
|
||||||
check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" }
|
check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" }
|
||||||
commands.removeAll { it.data is TimestampCommand }
|
commands.removeAll { it.data is TimestampCommand }
|
||||||
addCommand(TimestampCommand(time, 30.seconds), authenticatedBy.owningKey)
|
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 */
|
||||||
@ -115,17 +124,17 @@ class PartialTransaction(private val inputStates: MutableList<ContractStateRef>
|
|||||||
*
|
*
|
||||||
* The signature of the trusted timestamper merely asserts that the time field of this transaction is valid.
|
* The signature of the trusted timestamper merely asserts that the time field of this transaction is valid.
|
||||||
*/
|
*/
|
||||||
fun timestamp(timestamper: TimestamperService) {
|
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.
|
// TODO: Once we switch to a more advanced bytecode rewriting framework, we can call into a real implementation.
|
||||||
check(timestamper.javaClass.simpleName == "DummyTimestamper")
|
check(timestamper.javaClass.simpleName == "DummyTimestamper")
|
||||||
val t = time ?: throw IllegalStateException("Timestamping requested but no time was inserted into the transaction")
|
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.
|
// Obviously this is just a hard-coded dummy value for now.
|
||||||
val maxExpectedLatency = 5.seconds
|
val maxExpectedLatency = 5.seconds
|
||||||
if (Duration.between(Instant.now(), t.before) > maxExpectedLatency)
|
if (Duration.between(clock.instant(), t.before) > maxExpectedLatency)
|
||||||
throw TooLateException()
|
throw NotOnTimeException()
|
||||||
|
|
||||||
// The timestamper may also throw TooLateException if our clocks are desynchronised or if we are right on the
|
// 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
|
// 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.
|
// to GPS time i.e. the United States Naval Observatory.
|
||||||
val sig = timestamper.timestamp(toWireTransaction().serialize())
|
val sig = timestamper.timestamp(toWireTransaction().serialize())
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -72,6 +74,28 @@ class CommercialPaperTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 {
|
||||||
@ -109,7 +133,7 @@ class CommercialPaperTests {
|
|||||||
// 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).apply {
|
val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply {
|
||||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity)
|
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||||
signWith(MINI_CORP_KEY)
|
signWith(MINI_CORP_KEY)
|
||||||
timestamp(DUMMY_TIMESTAMPER)
|
timestamp(DUMMY_TIMESTAMPER)
|
||||||
}
|
}
|
||||||
@ -142,7 +166,7 @@ class CommercialPaperTests {
|
|||||||
|
|
||||||
fun makeRedeemTX(time: Instant): LedgerTransaction {
|
fun makeRedeemTX(time: Instant): LedgerTransaction {
|
||||||
val ptx = PartialTransaction()
|
val ptx = PartialTransaction()
|
||||||
ptx.setTime(time, DummyTimestampingAuthority.identity )
|
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)
|
||||||
|
@ -108,7 +108,7 @@ class CrowdFundTests {
|
|||||||
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).apply {
|
val ptx = CrowdFund().craftRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days).apply {
|
||||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity)
|
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||||
signWith(MINI_CORP_KEY)
|
signWith(MINI_CORP_KEY)
|
||||||
timestamp(DUMMY_TIMESTAMPER)
|
timestamp(DUMMY_TIMESTAMPER)
|
||||||
}
|
}
|
||||||
@ -128,7 +128,7 @@ 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)
|
ptx.setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||||
ptx.signWith(ALICE_KEY)
|
ptx.signWith(ALICE_KEY)
|
||||||
ptx.timestamp(DUMMY_TIMESTAMPER)
|
ptx.timestamp(DUMMY_TIMESTAMPER)
|
||||||
val stx = ptx.toSignedTransaction()
|
val stx = ptx.toSignedTransaction()
|
||||||
@ -144,7 +144,7 @@ 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)
|
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)
|
ptx.timestamp(DUMMY_TIMESTAMPER)
|
||||||
|
@ -85,7 +85,7 @@ class TransactionSerializationTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun timestamp() {
|
fun timestamp() {
|
||||||
tx.setTime(TEST_TX_TIME, DUMMY_TIMESTAMPER.identity)
|
tx.setTime(TEST_TX_TIME, DUMMY_TIMESTAMPER.identity, 30.seconds)
|
||||||
tx.timestamp(DUMMY_TIMESTAMPER)
|
tx.timestamp(DUMMY_TIMESTAMPER)
|
||||||
tx.signWith(TestUtils.keypair)
|
tx.signWith(TestUtils.keypair)
|
||||||
val stx = tx.toSignedTransaction()
|
val stx = tx.toSignedTransaction()
|
||||||
|
@ -78,7 +78,7 @@ class DummyTimestamper(var clock: Clock = Clock.fixed(TEST_TX_TIME, ZoneId.syste
|
|||||||
val wtx = wtxBytes.deserialize()
|
val wtx = wtxBytes.deserialize()
|
||||||
val timestamp = wtx.commands.mapNotNull { it.data as? TimestampCommand }.single()
|
val timestamp = wtx.commands.mapNotNull { it.data as? TimestampCommand }.single()
|
||||||
if (Duration.between(timestamp.before, clock.instant()) > tolerance)
|
if (Duration.between(timestamp.before, clock.instant()) > tolerance)
|
||||||
throw TooLateException()
|
throw NotOnTimeException()
|
||||||
return DummyTimestampingAuthority.key.signWithECDSA(wtxBytes.bits, identity)
|
return DummyTimestampingAuthority.key.signWithECDSA(wtxBytes.bits, identity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -182,7 +182,12 @@ abstract class AbstractTransactionForTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun timestamp(time: Instant) {
|
fun timestamp(time: Instant) {
|
||||||
commands.add(Command(TimestampCommand(time, 30.seconds), DUMMY_TIMESTAMPER.identity.owningKey))
|
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 { ... } }
|
||||||
|
Reference in New Issue
Block a user