Add a unit test to check before/after processing by the dummy timestamper.

Tweak the timestamping API a bit.
This commit is contained in:
Mike Hearn 2015-12-22 15:03:11 +00:00
parent 784452ac50
commit bcc56859af
5 changed files with 54 additions and 16 deletions

View File

@ -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())

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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 { ... } }