mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
CORDA-1353: Notaries should reject transactions that contain too… (#2986)
CORDA-1353: Notaries should reject transactions that contain too many inputs. Otherwise, it may cause a slowdown or make the service hang.
This commit is contained in:
parent
f89b3b3e0f
commit
7ad19af93f
@ -131,19 +131,21 @@ class NotaryFlow {
|
||||
*/
|
||||
// See AbstractStateReplacementFlow.Acceptor for why it's Void?
|
||||
abstract class Service(val otherSideSession: FlowSession, val service: TrustedAuthorityNotaryService) : FlowLogic<Void?>() {
|
||||
companion object {
|
||||
// TODO: Determine an appropriate limit and also enforce in the network parameters and the transaction builder.
|
||||
private const val maxAllowedInputs = 10_000
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
check(serviceHub.myInfo.legalIdentities.any { serviceHub.networkMapCache.isNotary(it) }) {
|
||||
"We are not a notary on the network"
|
||||
}
|
||||
|
||||
val requestPayload = otherSideSession.receive<NotarisationPayload>().unwrap { it }
|
||||
var txId: SecureHash? = null
|
||||
try {
|
||||
val parts = validateRequest(requestPayload)
|
||||
txId = parts.id
|
||||
checkNotary(parts.notary)
|
||||
service.validateTimeWindow(parts.timestamp)
|
||||
service.commitInputStates(parts.inputs, txId, otherSideSession.counterparty, requestPayload.requestSignature)
|
||||
signTransactionAndSendResponse(txId)
|
||||
@ -153,6 +155,16 @@ class NotaryFlow {
|
||||
return null
|
||||
}
|
||||
|
||||
/** Checks whether the number of input states is too large. */
|
||||
protected fun checkInputs(inputs: List<StateRef>) {
|
||||
if (inputs.size > maxAllowedInputs) {
|
||||
val error = NotaryError.TransactionInvalid(
|
||||
IllegalArgumentException("A transaction cannot have more than $maxAllowedInputs inputs, received: ${inputs.size}")
|
||||
)
|
||||
throw NotaryInternalException(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement custom logic to perform transaction verification based on validity and privacy requirements.
|
||||
*/
|
||||
|
@ -22,9 +22,12 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAut
|
||||
@Suspendable
|
||||
override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts {
|
||||
val transaction = requestPayload.coreTransaction
|
||||
checkInputs(transaction.inputs)
|
||||
val request = NotarisationRequest(transaction.inputs, transaction.id)
|
||||
validateRequestSignature(request, requestPayload.requestSignature)
|
||||
return extractParts(transaction)
|
||||
val parts = extractParts(transaction)
|
||||
checkNotary(parts.notary)
|
||||
return parts
|
||||
}
|
||||
|
||||
private fun extractParts(tx: CoreTransaction): TransactionParts {
|
||||
@ -35,8 +38,7 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAut
|
||||
checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP)
|
||||
checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP)
|
||||
}
|
||||
val notary = tx.notary
|
||||
TransactionParts(tx.id, tx.inputs, tx.timeWindow, notary)
|
||||
TransactionParts(tx.id, tx.inputs, tx.timeWindow, tx.notary)
|
||||
}
|
||||
is ContractUpgradeFilteredTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary)
|
||||
is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary)
|
||||
|
@ -27,6 +27,7 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
|
||||
override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts {
|
||||
try {
|
||||
val stx = requestPayload.signedTransaction
|
||||
checkInputs(stx.inputs)
|
||||
validateRequestSignature(NotarisationRequest(stx.inputs, stx.id), requestPayload.requestSignature)
|
||||
val notary = stx.notary
|
||||
checkNotary(notary)
|
||||
|
@ -1,38 +1,27 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.generateSignature
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
import net.corda.core.internal.NotaryChangeTransactionBuilder
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.messaging.Message
|
||||
import net.corda.node.services.statemachine.InitialSessionMessage
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.dummyCommand
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.internal.*
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import net.corda.testing.node.MockNetworkNotarySpec
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.InternalMockNodeParameters
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NotaryServiceTests {
|
||||
private lateinit var mockNet: InternalMockNetwork
|
||||
@ -43,7 +32,10 @@ class NotaryServiceTests {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts"))
|
||||
mockNet = InternalMockNetwork(
|
||||
cordappPackages = listOf("net.corda.testing.contracts"),
|
||||
notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME, validating = false))
|
||||
)
|
||||
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
|
||||
notaryServices = mockNet.defaultNotaryNode.services //TODO get rid of that
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
@ -56,201 +48,38 @@ class NotaryServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should sign a unique transaction with a valid time-window`() {
|
||||
val stx = run {
|
||||
val inputState = issueState(aliceNode.services, alice)
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
.setTimeWindow(Instant.now(), 30.seconds)
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val future = runNotaryClient(stx)
|
||||
val signatures = future.getOrThrow()
|
||||
signatures.forEach { it.verify(stx.id) }
|
||||
fun `should reject a transaction with too many inputs`() {
|
||||
notariseWithTooManyInputs(aliceNode, alice, notary, mockNet)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should sign a unique transaction without a time-window`() {
|
||||
val stx = run {
|
||||
val inputState = issueState(aliceNode.services, alice)
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
internal companion object {
|
||||
/** This is used by both [NotaryServiceTests] and [ValidatingNotaryServiceTests]. */
|
||||
fun notariseWithTooManyInputs(node: StartedNode<InternalMockNetwork.MockNode>, party: Party, notary: Party, network: InternalMockNetwork) {
|
||||
val stx = generateTransaction(node, party, notary)
|
||||
|
||||
val future = node.services.startFlow(DummyClientFlow(stx, notary)).resultFuture
|
||||
network.runNetwork()
|
||||
assertFailsWith<NotaryException> { future.getOrThrow() }
|
||||
}
|
||||
|
||||
val future = runNotaryClient(stx)
|
||||
val signatures = future.getOrThrow()
|
||||
signatures.forEach { it.verify(stx.id) }
|
||||
}
|
||||
private fun generateTransaction(node: StartedNode<InternalMockNetwork.MockNode>, party: Party, notary: Party): SignedTransaction {
|
||||
val inputs = (1..10_005).map { StateRef(SecureHash.randomSHA256(), 0) }
|
||||
val tx = NotaryChangeTransactionBuilder(inputs, notary, party).build()
|
||||
|
||||
@Test
|
||||
fun `should report error for transaction with an invalid time-window`() {
|
||||
val stx = run {
|
||||
val inputState = issueState(aliceNode.services, alice)
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
.setTimeWindow(Instant.now().plusSeconds(3600), 30.seconds)
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val future = runNotaryClient(stx)
|
||||
|
||||
val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() }
|
||||
assertThat(ex.error).isInstanceOf(NotaryError.TimeWindowInvalid::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should sign identical transaction multiple times (notarisation is idempotent)`() {
|
||||
val stx = run {
|
||||
val inputState = issueState(aliceNode.services, alice)
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val firstAttempt = NotaryFlow.Client(stx)
|
||||
val secondAttempt = NotaryFlow.Client(stx)
|
||||
val f1 = aliceNode.services.startFlow(firstAttempt).resultFuture
|
||||
val f2 = aliceNode.services.startFlow(secondAttempt).resultFuture
|
||||
|
||||
mockNet.runNetwork()
|
||||
|
||||
// Note that the notary will only return identical signatures when using deterministic signature
|
||||
// schemes (e.g. EdDSA) and when deterministic metadata is attached (no timestamps or nonces).
|
||||
// We only really care that both signatures are over the same transaction and by the same notary.
|
||||
val sig1 = f1.getOrThrow().single()
|
||||
assertEquals(sig1.by, notary.owningKey)
|
||||
assertTrue(sig1.isValid(stx.id))
|
||||
|
||||
val sig2 = f2.getOrThrow().single()
|
||||
assertEquals(sig2.by, notary.owningKey)
|
||||
assertTrue(sig2.isValid(stx.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should report conflict when inputs are reused across transactions`() {
|
||||
val firstState = issueState(aliceNode.services, alice)
|
||||
val secondState = issueState(aliceNode.services, alice)
|
||||
|
||||
fun spendState(state: StateAndRef<*>): SignedTransaction {
|
||||
val stx = run {
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(state)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
return node.services.run {
|
||||
val myKey = myInfo.legalIdentities.first().owningKey
|
||||
val signableData = SignableData(tx.id, SignatureMetadata(myInfo.platformVersion, Crypto.findSignatureScheme(myKey).schemeNumberID))
|
||||
val mySignature = keyManagementService.sign(signableData, myKey)
|
||||
SignedTransaction(tx, listOf(mySignature))
|
||||
}
|
||||
aliceNode.services.startFlow(NotaryFlow.Client(stx))
|
||||
mockNet.runNetwork()
|
||||
return stx
|
||||
}
|
||||
|
||||
val firstSpendTx = spendState(firstState)
|
||||
val secondSpendTx = spendState(secondState)
|
||||
|
||||
val doubleSpendTx = run {
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(issueState(aliceNode.services, alice))
|
||||
.addInputState(firstState)
|
||||
.addInputState(secondState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val doubleSpend = NotaryFlow.Client(doubleSpendTx) // Double spend the inputState in a second transaction.
|
||||
val future = aliceNode.services.startFlow(doubleSpend)
|
||||
mockNet.runNetwork()
|
||||
|
||||
val ex = assertFailsWith(NotaryException::class) { future.resultFuture.getOrThrow() }
|
||||
val notaryError = ex.error as NotaryError.Conflict
|
||||
assertEquals(notaryError.txId, doubleSpendTx.id)
|
||||
with(notaryError) {
|
||||
assertEquals(consumedStates.size, 2)
|
||||
assertEquals(consumedStates[firstState.ref]!!.hashOfTransactionId, firstSpendTx.id.sha256())
|
||||
assertEquals(consumedStates[secondState.ref]!!.hashOfTransactionId, secondSpendTx.id.sha256())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject when notarisation request not signed by the requesting party`() {
|
||||
runNotarisationAndInterceptClientPayload { originalPayload ->
|
||||
val transaction = originalPayload.signedTransaction
|
||||
val randomKeyPair = Crypto.generateKeyPair()
|
||||
val bytesToSign = NotarisationRequest(transaction.inputs, transaction.id).serialize().bytes
|
||||
val modifiedSignature = NotarisationRequestSignature(randomKeyPair.sign(bytesToSign), aliceNode.services.myInfo.platformVersion)
|
||||
originalPayload.copy(requestSignature = modifiedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject when incorrect notarisation request signed - inputs don't match`() {
|
||||
runNotarisationAndInterceptClientPayload { originalPayload ->
|
||||
val transaction = originalPayload.signedTransaction
|
||||
val wrongInputs = listOf(StateRef(SecureHash.randomSHA256(), 0))
|
||||
val request = NotarisationRequest(wrongInputs, transaction.id)
|
||||
val modifiedSignature = request.generateSignature(aliceNode.services)
|
||||
originalPayload.copy(requestSignature = modifiedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject when incorrect notarisation request signed - transaction id doesn't match`() {
|
||||
runNotarisationAndInterceptClientPayload { originalPayload ->
|
||||
val transaction = originalPayload.signedTransaction
|
||||
val wrongTransactionId = SecureHash.randomSHA256()
|
||||
val request = NotarisationRequest(transaction.inputs, wrongTransactionId)
|
||||
val modifiedSignature = request.generateSignature(aliceNode.services)
|
||||
originalPayload.copy(requestSignature = modifiedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runNotarisationAndInterceptClientPayload(payloadModifier: (NotarisationPayload) -> NotarisationPayload) {
|
||||
aliceNode.setMessagingServiceSpy(object : MessagingServiceSpy(aliceNode.network) {
|
||||
override fun send(message: Message, target: MessageRecipients, retryId: Long?, sequenceKey: Any) {
|
||||
val messageData = message.data.deserialize<Any>() as? InitialSessionMessage
|
||||
val payload = messageData?.firstPayload!!.deserialize()
|
||||
|
||||
if (payload is NotarisationPayload) {
|
||||
val alteredPayload = payloadModifier(payload)
|
||||
val alteredMessageData = messageData.copy(firstPayload = alteredPayload.serialize())
|
||||
val alteredMessage = InMemoryMessage(message.topic, OpaqueBytes(alteredMessageData.serialize().bytes), message.uniqueMessageId)
|
||||
messagingService.send(alteredMessage, target, retryId)
|
||||
|
||||
} else {
|
||||
messagingService.send(message, target, retryId)
|
||||
}
|
||||
private class DummyClientFlow(stx: SignedTransaction, val notary: Party) : NotaryFlow.Client(stx) {
|
||||
@Suspendable
|
||||
override fun call(): List<TransactionSignature> {
|
||||
notarise(notary)
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
})
|
||||
|
||||
val stx = run {
|
||||
val inputState = issueState(aliceNode.services, alice)
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val future = runNotaryClient(stx)
|
||||
val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() }
|
||||
assertThat(ex.error).isInstanceOf(NotaryError.RequestSignatureInvalid::class.java)
|
||||
}
|
||||
|
||||
private fun runNotaryClient(stx: SignedTransaction): CordaFuture<List<TransactionSignature>> {
|
||||
val flow = NotaryFlow.Client(stx)
|
||||
val future = aliceNode.services.startFlow(flow).resultFuture
|
||||
mockNet.runNetwork()
|
||||
return future
|
||||
}
|
||||
|
||||
private fun issueState(services: ServiceHub, identity: Party): StateAndRef<*> {
|
||||
val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0))
|
||||
val signedByNode = services.signInitialTransaction(tx)
|
||||
val stx = notaryServices.addSignature(signedByNode, notary.owningKey)
|
||||
services.recordTransactions(stx)
|
||||
return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0))
|
||||
}
|
||||
}
|
||||
|
@ -4,43 +4,49 @@ import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.generateSignature
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.issueInvalidState
|
||||
import net.corda.node.services.messaging.Message
|
||||
import net.corda.node.services.statemachine.InitialSessionMessage
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.dummyCommand
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNodeParameters
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import net.corda.testing.node.internal.*
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ValidatingNotaryServiceTests {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var notaryNode: StartedMockNode
|
||||
private lateinit var aliceNode: StartedMockNode
|
||||
private lateinit var mockNet: InternalMockNetwork
|
||||
private lateinit var notaryNode: StartedNode<InternalMockNetwork.MockNode>
|
||||
private lateinit var aliceNode: StartedNode<InternalMockNetwork.MockNode>
|
||||
private lateinit var notary: Party
|
||||
private lateinit var alice: Party
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts"))
|
||||
aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME))
|
||||
mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts"))
|
||||
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
|
||||
notaryNode = mockNet.defaultNotaryNode
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
alice = aliceNode.info.singleIdentity()
|
||||
@ -61,7 +67,7 @@ class ValidatingNotaryServiceTests {
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val future = runClient(stx)
|
||||
val future = runNotaryClient(stx)
|
||||
|
||||
val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() }
|
||||
val notaryError = ex.error as NotaryError.TransactionInvalid
|
||||
@ -82,16 +88,205 @@ class ValidatingNotaryServiceTests {
|
||||
// Expecting SignaturesMissingException instead of NotaryException, since the exception should originate from
|
||||
// the client flow.
|
||||
val ex = assertFailsWith<SignedTransaction.SignaturesMissingException> {
|
||||
val future = runClient(stx)
|
||||
val future = runNotaryClient(stx)
|
||||
future.getOrThrow()
|
||||
}
|
||||
val missingKeys = ex.missing
|
||||
assertEquals(setOf(expectedMissingKey), missingKeys)
|
||||
}
|
||||
|
||||
private fun runClient(stx: SignedTransaction): CordaFuture<List<TransactionSignature>> {
|
||||
@Test
|
||||
fun `should sign a unique transaction with a valid time-window`() {
|
||||
val stx = run {
|
||||
val inputState = issueState(aliceNode.services, alice)
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
.setTimeWindow(Instant.now(), 30.seconds)
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val future = runNotaryClient(stx)
|
||||
val signatures = future.getOrThrow()
|
||||
signatures.forEach { it.verify(stx.id) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should sign a unique transaction without a time-window`() {
|
||||
val stx = run {
|
||||
val inputState = issueState(aliceNode.services, alice)
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val future = runNotaryClient(stx)
|
||||
val signatures = future.getOrThrow()
|
||||
signatures.forEach { it.verify(stx.id) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should report error for transaction with an invalid time-window`() {
|
||||
val stx = run {
|
||||
val inputState = issueState(aliceNode.services, alice)
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
.setTimeWindow(Instant.now().plusSeconds(3600), 30.seconds)
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val future = runNotaryClient(stx)
|
||||
|
||||
val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() }
|
||||
assertThat(ex.error).isInstanceOf(NotaryError.TimeWindowInvalid::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should sign identical transaction multiple times (notarisation is idempotent)`() {
|
||||
val stx = run {
|
||||
val inputState = issueState(aliceNode.services, alice)
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val firstAttempt = NotaryFlow.Client(stx)
|
||||
val secondAttempt = NotaryFlow.Client(stx)
|
||||
val f1 = aliceNode.services.startFlow(firstAttempt).resultFuture
|
||||
val f2 = aliceNode.services.startFlow(secondAttempt).resultFuture
|
||||
|
||||
mockNet.runNetwork()
|
||||
|
||||
// Note that the notary will only return identical signatures when using deterministic signature
|
||||
// schemes (e.g. EdDSA) and when deterministic metadata is attached (no timestamps or nonces).
|
||||
// We only really care that both signatures are over the same transaction and by the same notary.
|
||||
val sig1 = f1.getOrThrow().single()
|
||||
assertEquals(sig1.by, notary.owningKey)
|
||||
assertTrue(sig1.isValid(stx.id))
|
||||
|
||||
val sig2 = f2.getOrThrow().single()
|
||||
assertEquals(sig2.by, notary.owningKey)
|
||||
assertTrue(sig2.isValid(stx.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should report conflict when inputs are reused across transactions`() {
|
||||
val firstState = issueState(aliceNode.services, alice)
|
||||
val secondState = issueState(aliceNode.services, alice)
|
||||
|
||||
fun spendState(state: StateAndRef<*>): SignedTransaction {
|
||||
val stx = run {
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(state)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
aliceNode.services.startFlow(NotaryFlow.Client(stx))
|
||||
mockNet.runNetwork()
|
||||
return stx
|
||||
}
|
||||
|
||||
val firstSpendTx = spendState(firstState)
|
||||
val secondSpendTx = spendState(secondState)
|
||||
|
||||
val doubleSpendTx = run {
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(issueState(aliceNode.services, alice))
|
||||
.addInputState(firstState)
|
||||
.addInputState(secondState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val doubleSpend = NotaryFlow.Client(doubleSpendTx) // Double spend the inputState in a second transaction.
|
||||
val future = aliceNode.services.startFlow(doubleSpend)
|
||||
mockNet.runNetwork()
|
||||
|
||||
val ex = assertFailsWith(NotaryException::class) { future.resultFuture.getOrThrow() }
|
||||
val notaryError = ex.error as NotaryError.Conflict
|
||||
assertEquals(notaryError.txId, doubleSpendTx.id)
|
||||
with(notaryError) {
|
||||
assertEquals(consumedStates.size, 2)
|
||||
assertEquals(consumedStates[firstState.ref]!!.hashOfTransactionId, firstSpendTx.id.sha256())
|
||||
assertEquals(consumedStates[secondState.ref]!!.hashOfTransactionId, secondSpendTx.id.sha256())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject when notarisation request not signed by the requesting party`() {
|
||||
runNotarisationAndInterceptClientPayload { originalPayload ->
|
||||
val transaction = originalPayload.signedTransaction
|
||||
val randomKeyPair = Crypto.generateKeyPair()
|
||||
val bytesToSign = NotarisationRequest(transaction.inputs, transaction.id).serialize().bytes
|
||||
val modifiedSignature = NotarisationRequestSignature(randomKeyPair.sign(bytesToSign), aliceNode.services.myInfo.platformVersion)
|
||||
originalPayload.copy(requestSignature = modifiedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject when incorrect notarisation request signed - inputs don't match`() {
|
||||
runNotarisationAndInterceptClientPayload { originalPayload ->
|
||||
val transaction = originalPayload.signedTransaction
|
||||
val wrongInputs = listOf(StateRef(SecureHash.randomSHA256(), 0))
|
||||
val request = NotarisationRequest(wrongInputs, transaction.id)
|
||||
val modifiedSignature = request.generateSignature(aliceNode.services)
|
||||
originalPayload.copy(requestSignature = modifiedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject when incorrect notarisation request signed - transaction id doesn't match`() {
|
||||
runNotarisationAndInterceptClientPayload { originalPayload ->
|
||||
val transaction = originalPayload.signedTransaction
|
||||
val wrongTransactionId = SecureHash.randomSHA256()
|
||||
val request = NotarisationRequest(transaction.inputs, wrongTransactionId)
|
||||
val modifiedSignature = request.generateSignature(aliceNode.services)
|
||||
originalPayload.copy(requestSignature = modifiedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject a transaction with too many inputs`() {
|
||||
NotaryServiceTests.notariseWithTooManyInputs(aliceNode, alice, notary, mockNet)
|
||||
}
|
||||
|
||||
private fun runNotarisationAndInterceptClientPayload(payloadModifier: (NotarisationPayload) -> NotarisationPayload) {
|
||||
aliceNode.setMessagingServiceSpy(object : MessagingServiceSpy(aliceNode.network) {
|
||||
override fun send(message: Message, target: MessageRecipients, retryId: Long?, sequenceKey: Any) {
|
||||
val messageData = message.data.deserialize<Any>() as? InitialSessionMessage
|
||||
val payload = messageData?.firstPayload!!.deserialize()
|
||||
|
||||
if (payload is NotarisationPayload) {
|
||||
val alteredPayload = payloadModifier(payload)
|
||||
val alteredMessageData = messageData.copy(firstPayload = alteredPayload.serialize())
|
||||
val alteredMessage = InMemoryMessage(message.topic, OpaqueBytes(alteredMessageData.serialize().bytes), message.uniqueMessageId)
|
||||
messagingService.send(alteredMessage, target, retryId)
|
||||
|
||||
} else {
|
||||
messagingService.send(message, target, retryId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val stx = run {
|
||||
val inputState = issueState(aliceNode.services, alice)
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val future = runNotaryClient(stx)
|
||||
val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() }
|
||||
assertThat(ex.error).isInstanceOf(NotaryError.RequestSignatureInvalid::class.java)
|
||||
}
|
||||
|
||||
private fun runNotaryClient(stx: SignedTransaction): CordaFuture<List<TransactionSignature>> {
|
||||
val flow = NotaryFlow.Client(stx)
|
||||
val future = aliceNode.startFlow(flow)
|
||||
val future = aliceNode.services.startFlow(flow).resultFuture
|
||||
mockNet.runNetwork()
|
||||
return future
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user