mirror of
https://github.com/corda/corda.git
synced 2025-02-20 09:26:41 +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:
parent
784452ac50
commit
bcc56859af
@ -14,6 +14,7 @@ import core.serialization.serialize
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
@ -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
|
||||
* 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. */
|
||||
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.
|
||||
* 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" }
|
||||
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 */
|
||||
@ -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.
|
||||
*/
|
||||
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.
|
||||
check(timestamper.javaClass.simpleName == "DummyTimestamper")
|
||||
val t = time ?: throw IllegalStateException("Timestamping requested but no time was inserted into the transaction")
|
||||
|
||||
// Obviously this is just a hard-coded dummy value for now.
|
||||
val maxExpectedLatency = 5.seconds
|
||||
if (Duration.between(Instant.now(), t.before) > maxExpectedLatency)
|
||||
throw TooLateException()
|
||||
if (Duration.between(clock.instant(), t.before) > maxExpectedLatency)
|
||||
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
|
||||
// to GPS time i.e. the United States Naval Observatory.
|
||||
val sig = timestamper.timestamp(toWireTransaction().serialize())
|
||||
|
@ -11,7 +11,9 @@ package contracts
|
||||
import core.*
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@ -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
|
||||
fun `issue cannot replace an existing state`() {
|
||||
transactionGroup {
|
||||
@ -109,7 +133,7 @@ class CommercialPaperTests {
|
||||
// MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
|
||||
val issueTX: LedgerTransaction = run {
|
||||
val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply {
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity)
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
timestamp(DUMMY_TIMESTAMPER)
|
||||
}
|
||||
@ -142,7 +166,7 @@ class CommercialPaperTests {
|
||||
|
||||
fun makeRedeemTX(time: Instant): LedgerTransaction {
|
||||
val ptx = PartialTransaction()
|
||||
ptx.setTime(time, DummyTimestampingAuthority.identity )
|
||||
ptx.setTime(time, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
CommercialPaper().craftRedeem(ptx, moveTX.outRef(1), corpWallet)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
|
@ -108,7 +108,7 @@ class CrowdFundTests {
|
||||
val registerTX: LedgerTransaction = run {
|
||||
// craftRegister returns a partial transaction
|
||||
val ptx = CrowdFund().craftRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days).apply {
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity)
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
timestamp(DUMMY_TIMESTAMPER)
|
||||
}
|
||||
@ -128,7 +128,7 @@ class CrowdFundTests {
|
||||
val ptx = PartialTransaction()
|
||||
CrowdFund().craftPledge(ptx, registerTX.outRef(0), ALICE)
|
||||
Cash().craftSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet)
|
||||
ptx.setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity)
|
||||
ptx.setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.timestamp(DUMMY_TIMESTAMPER)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
@ -144,7 +144,7 @@ class CrowdFundTests {
|
||||
// MiniCorp closes their campaign.
|
||||
fun makeFundedTX(time: Instant): LedgerTransaction {
|
||||
val ptx = PartialTransaction()
|
||||
ptx.setTime(time, DUMMY_TIMESTAMPER.identity)
|
||||
ptx.setTime(time, DUMMY_TIMESTAMPER.identity, 30.seconds)
|
||||
CrowdFund().craftClose(ptx, pledgeTX.outRef(0), miniCorpWallet)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.timestamp(DUMMY_TIMESTAMPER)
|
||||
|
@ -85,7 +85,7 @@ class TransactionSerializationTests {
|
||||
|
||||
@Test
|
||||
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.signWith(TestUtils.keypair)
|
||||
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 timestamp = wtx.commands.mapNotNull { it.data as? TimestampCommand }.single()
|
||||
if (Duration.between(timestamp.before, clock.instant()) > tolerance)
|
||||
throw TooLateException()
|
||||
throw NotOnTimeException()
|
||||
return DummyTimestampingAuthority.key.signWithECDSA(wtxBytes.bits, identity)
|
||||
}
|
||||
}
|
||||
@ -182,7 +182,12 @@ abstract class AbstractTransactionForTest {
|
||||
}
|
||||
|
||||
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 { ... } }
|
||||
|
Loading…
x
Reference in New Issue
Block a user