mirror of
https://github.com/corda/corda.git
synced 2024-12-28 00:38:55 +00:00
commit
ac328aeb67
@ -1384,12 +1384,10 @@ public static final class net.corda.core.flows.NotarisationRequest$Companion ext
|
||||
@net.corda.core.serialization.CordaSerializable public abstract class net.corda.core.flows.NotaryError extends java.lang.Object
|
||||
##
|
||||
@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.flows.NotaryError$Conflict extends net.corda.core.flows.NotaryError
|
||||
public <init>(net.corda.core.crypto.SecureHash, net.corda.core.crypto.SignedData)
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignedData component2()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$Conflict copy(net.corda.core.crypto.SecureHash, net.corda.core.crypto.SignedData)
|
||||
@org.jetbrains.annotations.NotNull public final Map component2()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$Conflict copy(net.corda.core.crypto.SecureHash, Map)
|
||||
public boolean equals(Object)
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignedData getConflict()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTxId()
|
||||
public int hashCode()
|
||||
@org.jetbrains.annotations.NotNull public String toString()
|
||||
@ -1440,13 +1438,13 @@ public static final class net.corda.core.flows.NotaryError$TimeWindowInvalid$Com
|
||||
public static final net.corda.core.flows.NotaryError$WrongNotary INSTANCE
|
||||
##
|
||||
@net.corda.core.serialization.CordaSerializable public final class net.corda.core.flows.NotaryException extends net.corda.core.flows.FlowException
|
||||
public <init>(net.corda.core.flows.NotaryError)
|
||||
public <init>(net.corda.core.flows.NotaryError, net.corda.core.crypto.SecureHash)
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError getError()
|
||||
##
|
||||
public final class net.corda.core.flows.NotaryFlow extends java.lang.Object
|
||||
public <init>()
|
||||
##
|
||||
@net.corda.core.flows.InitiatingFlow public static class net.corda.core.flows.NotaryFlow$Client extends net.corda.core.flows.FlowLogic
|
||||
@net.corda.core.DoNotImplement @net.corda.core.flows.InitiatingFlow public static class net.corda.core.flows.NotaryFlow$Client extends net.corda.core.flows.FlowLogic
|
||||
public <init>(net.corda.core.transactions.SignedTransaction)
|
||||
public <init>(net.corda.core.transactions.SignedTransaction, net.corda.core.utilities.ProgressTracker)
|
||||
@co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call()
|
||||
@ -3015,17 +3013,15 @@ public static final class net.corda.core.serialization.SingletonSerializationTok
|
||||
@org.jetbrains.annotations.NotNull public final String getReason()
|
||||
##
|
||||
@net.corda.core.DoNotImplement @net.corda.core.serialization.CordaSerializable public final class net.corda.core.transactions.ContractUpgradeFilteredTransaction extends net.corda.core.transactions.CoreTransaction
|
||||
public <init>(List, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash)
|
||||
@org.jetbrains.annotations.NotNull public final List component1()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component2()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component3()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.ContractUpgradeFilteredTransaction copy(List, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash)
|
||||
public <init>(Map, Map)
|
||||
@org.jetbrains.annotations.NotNull public final Map component1()
|
||||
@org.jetbrains.annotations.NotNull public final Map component2()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.ContractUpgradeFilteredTransaction copy(Map, Map)
|
||||
public boolean equals(Object)
|
||||
@org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId()
|
||||
@org.jetbrains.annotations.NotNull public List getInputs()
|
||||
@org.jetbrains.annotations.NotNull public net.corda.core.identity.Party getNotary()
|
||||
@org.jetbrains.annotations.NotNull public List getOutputs()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getRest()
|
||||
public int hashCode()
|
||||
public String toString()
|
||||
##
|
||||
@ -3052,8 +3048,8 @@ public static final class net.corda.core.serialization.SingletonSerializationTok
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt getPrivacySalt()
|
||||
@org.jetbrains.annotations.NotNull public Set getRequiredSigningKeys()
|
||||
@org.jetbrains.annotations.NotNull public List getSigs()
|
||||
@org.jetbrains.annotations.NotNull public final String getUpgradeContractClassName()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Attachment getUpgradedContractAttachment()
|
||||
@org.jetbrains.annotations.NotNull public final String getUpgradedContractClassName()
|
||||
public int hashCode()
|
||||
public String toString()
|
||||
public void verifyRequiredSignatures()
|
||||
@ -3061,15 +3057,11 @@ public static final class net.corda.core.serialization.SingletonSerializationTok
|
||||
public void verifySignaturesExcept(java.security.PublicKey...)
|
||||
##
|
||||
@net.corda.core.DoNotImplement @net.corda.core.serialization.CordaSerializable public final class net.corda.core.transactions.ContractUpgradeWireTransaction extends net.corda.core.transactions.CoreTransaction
|
||||
public <init>(List, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash, String, net.corda.core.crypto.SecureHash, net.corda.core.contracts.PrivacySalt)
|
||||
public <init>(List, net.corda.core.contracts.PrivacySalt)
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.ContractUpgradeFilteredTransaction buildFilteredTransaction()
|
||||
@org.jetbrains.annotations.NotNull public final List component1()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component2()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component3()
|
||||
@org.jetbrains.annotations.NotNull public final String component4()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component5()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt component6()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.ContractUpgradeWireTransaction copy(List, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash, String, net.corda.core.crypto.SecureHash, net.corda.core.contracts.PrivacySalt)
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt component2()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.ContractUpgradeWireTransaction copy(List, net.corda.core.contracts.PrivacySalt)
|
||||
public boolean equals(Object)
|
||||
@org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId()
|
||||
@org.jetbrains.annotations.NotNull public List getInputs()
|
||||
@ -3077,8 +3069,8 @@ public static final class net.corda.core.serialization.SingletonSerializationTok
|
||||
@org.jetbrains.annotations.NotNull public net.corda.core.identity.Party getNotary()
|
||||
@org.jetbrains.annotations.NotNull public List getOutputs()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt getPrivacySalt()
|
||||
@org.jetbrains.annotations.NotNull public final String getUpgradeContractClassName()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getUpgradedContractAttachmentId()
|
||||
@org.jetbrains.annotations.NotNull public final String getUpgradedContractClassName()
|
||||
public int hashCode()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.ContractUpgradeLedgerTransaction resolve(net.corda.core.node.ServicesForResolution, List)
|
||||
public String toString()
|
||||
@ -3213,11 +3205,9 @@ public static final class net.corda.core.transactions.LedgerTransaction$InOutGro
|
||||
public void verifySignaturesExcept(java.security.PublicKey...)
|
||||
##
|
||||
@net.corda.core.DoNotImplement @net.corda.core.serialization.CordaSerializable public final class net.corda.core.transactions.NotaryChangeWireTransaction extends net.corda.core.transactions.CoreTransaction
|
||||
public <init>(List, net.corda.core.identity.Party, net.corda.core.identity.Party)
|
||||
public <init>(List)
|
||||
@org.jetbrains.annotations.NotNull public final List component1()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component2()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component3()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeWireTransaction copy(List, net.corda.core.identity.Party, net.corda.core.identity.Party)
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeWireTransaction copy(List)
|
||||
public boolean equals(Object)
|
||||
@org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId()
|
||||
@org.jetbrains.annotations.NotNull public List getInputs()
|
||||
|
@ -86,7 +86,8 @@ class SwapIdentitiesFlow(private val otherParty: Party,
|
||||
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
|
||||
val serializedIdentity = SerializedBytes<PartyAndCertificate>(legalIdentityAnonymous.serialize().bytes)
|
||||
|
||||
// Special case that if we're both parties, a single identity is generated
|
||||
// Special case that if we're both parties, a single identity is generated.
|
||||
// TODO: for increased privacy, we should create one anonymous key per output state.
|
||||
val identities = LinkedHashMap<Party, AnonymousParty>()
|
||||
if (serviceHub.myInfo.isLegalIdentity(otherParty)) {
|
||||
identities.put(otherParty, legalIdentityAnonymous.party.anonymise())
|
||||
|
@ -179,7 +179,7 @@ fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Cr
|
||||
* which should never happen and suggests an unusual JVM or non-standard Java library.
|
||||
*/
|
||||
@Throws(NoSuchAlgorithmException::class)
|
||||
fun secureRandomBytes(numOfBytes: Int): ByteArray = newSecureRandom().generateSeed(numOfBytes)
|
||||
fun secureRandomBytes(numOfBytes: Int): ByteArray = ByteArray(numOfBytes).apply { newSecureRandom().nextBytes(this) }
|
||||
|
||||
/**
|
||||
* This is a hack added because during deserialisation when no-param constructors are called sometimes default values
|
||||
@ -257,7 +257,11 @@ fun componentHash(opaqueBytes: OpaqueBytes, privacySalt: PrivacySalt, componentG
|
||||
/** Return the SHA256(SHA256(nonce || serializedComponent)). */
|
||||
fun componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash = SecureHash.sha256Twice(nonce.bytes + opaqueBytes.bytes)
|
||||
|
||||
/** Serialise the object and return the hash of the serialized bytes. */
|
||||
/**
|
||||
* Serialise the object and return the hash of the serialized bytes. Note that the resulting hash may not be deterministic
|
||||
* across platform versions: serialization can produce different values if any of the types being serialized have changed,
|
||||
* or if the version of serialization specified by the context changes.
|
||||
*/
|
||||
fun <T : Any> serializedHash(x: T): SecureHash = x.serialize(context = SerializationDefaults.P2P_CONTEXT.withoutReferences()).bytes.sha256()
|
||||
|
||||
/**
|
||||
|
@ -88,7 +88,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
* Generates a random SHA-256 value.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun randomSHA256() = sha256(newSecureRandom().generateSeed(32))
|
||||
fun randomSHA256() = sha256(secureRandomBytes(32))
|
||||
|
||||
/**
|
||||
* A SHA-256 hash value consisting of 32 0x00 bytes.
|
||||
|
@ -11,8 +11,7 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.serialize
|
||||
@ -53,7 +52,7 @@ class NotarisationRequest(statesToConsume: List<StateRef>, val transactionId: Se
|
||||
val signature = requestSignature.digitalSignature
|
||||
if (intendedSigner.owningKey != signature.by) {
|
||||
val errorMessage = "Expected a signature by ${intendedSigner.owningKey}, but received by ${signature.by}}"
|
||||
throw NotaryException(NotaryError.RequestSignatureInvalid(IllegalArgumentException(errorMessage)))
|
||||
throw NotaryInternalException(NotaryError.RequestSignatureInvalid(IllegalArgumentException(errorMessage)))
|
||||
}
|
||||
// TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to
|
||||
// reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's
|
||||
@ -69,7 +68,7 @@ class NotarisationRequest(statesToConsume: List<StateRef>, val transactionId: Se
|
||||
when (e) {
|
||||
is InvalidKeyException, is SignatureException -> {
|
||||
val error = NotaryError.RequestSignatureInvalid(e)
|
||||
throw NotaryException(error)
|
||||
throw NotaryInternalException(error)
|
||||
}
|
||||
else -> throw e
|
||||
}
|
||||
@ -109,3 +108,7 @@ data class NotarisationPayload(val transaction: Any, val requestSignature: Notar
|
||||
*/
|
||||
val coreTransaction get() = transaction as CoreTransaction
|
||||
}
|
||||
|
||||
/** Payload returned by the notary service flow to the client. */
|
||||
@CordaSerializable
|
||||
data class NotarisationResponse(val signatures: List<TransactionSignature>)
|
@ -17,7 +17,7 @@ import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignableData
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.internal.NotaryChangeTransactionBuilder
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
@ -40,11 +40,11 @@ class NotaryChangeFlow<out T : ContractState>(
|
||||
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
|
||||
val inputs = resolveEncumbrances(originalState)
|
||||
|
||||
val tx = NotaryChangeWireTransaction(
|
||||
val tx = NotaryChangeTransactionBuilder(
|
||||
inputs.map { it.ref },
|
||||
originalState.state.notary,
|
||||
modification
|
||||
)
|
||||
).build()
|
||||
|
||||
val participantKeys = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet()
|
||||
// TODO: We need a much faster way of finding our key in the transaction
|
||||
|
@ -11,27 +11,24 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.internal.generateSignature
|
||||
import net.corda.core.internal.validateSignatures
|
||||
import net.corda.core.node.services.NotaryService
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.security.SignatureException
|
||||
import java.time.Instant
|
||||
import java.util.function.Predicate
|
||||
|
||||
@ -46,6 +43,7 @@ class NotaryFlow {
|
||||
* @throws NotaryException in case the any of the inputs to the transaction have been consumed
|
||||
* by another transaction or the time-window is invalid.
|
||||
*/
|
||||
@DoNotImplement
|
||||
@InitiatingFlow
|
||||
open class Client(private val stx: SignedTransaction,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<List<TransactionSignature>>() {
|
||||
@ -78,44 +76,32 @@ class NotaryFlow {
|
||||
check(serviceHub.loadStates(stx.inputs.toSet()).all { it.state.notary == notaryParty }) {
|
||||
"Input states must have the same Notary"
|
||||
}
|
||||
|
||||
try {
|
||||
stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
|
||||
} catch (ex: SignatureException) {
|
||||
throw NotaryException(NotaryError.TransactionInvalid(ex))
|
||||
}
|
||||
return notaryParty
|
||||
}
|
||||
|
||||
/** Notarises the transaction with the [notaryParty], obtains the notary's signature(s). */
|
||||
@Throws(NotaryException::class)
|
||||
@Suspendable
|
||||
protected fun notarise(notaryParty: Party): UntrustworthyData<List<TransactionSignature>> {
|
||||
return try {
|
||||
protected fun notarise(notaryParty: Party): UntrustworthyData<NotarisationResponse> {
|
||||
val session = initiateFlow(notaryParty)
|
||||
val requestSignature = NotarisationRequest(stx.inputs, stx.id).generateSignature(serviceHub)
|
||||
if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
||||
return if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
||||
sendAndReceiveValidating(session, requestSignature)
|
||||
} else {
|
||||
sendAndReceiveNonValidating(notaryParty, session, requestSignature)
|
||||
}
|
||||
} catch (e: NotaryException) {
|
||||
if (e.error is NotaryError.Conflict) {
|
||||
e.error.conflict.verified()
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun sendAndReceiveValidating(session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<List<TransactionSignature>> {
|
||||
private fun sendAndReceiveValidating(session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<NotarisationResponse> {
|
||||
val payload = NotarisationPayload(stx, signature)
|
||||
subFlow(NotarySendTransactionFlow(session, payload))
|
||||
return session.receive()
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<List<TransactionSignature>> {
|
||||
private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<NotarisationResponse> {
|
||||
val ctx = stx.coreTransaction
|
||||
val tx = when (ctx) {
|
||||
is ContractUpgradeWireTransaction -> ctx.buildFilteredTransaction()
|
||||
@ -126,18 +112,13 @@ class NotaryFlow {
|
||||
}
|
||||
|
||||
/** Checks that the notary's signature(s) is/are valid. */
|
||||
protected fun validateResponse(response: UntrustworthyData<List<TransactionSignature>>, notaryParty: Party): List<TransactionSignature> {
|
||||
return response.unwrap { signatures ->
|
||||
signatures.forEach { validateSignature(it, stx.id, notaryParty) }
|
||||
signatures
|
||||
protected fun validateResponse(response: UntrustworthyData<NotarisationResponse>, notaryParty: Party): List<TransactionSignature> {
|
||||
return response.unwrap {
|
||||
it.validateSignatures(stx.id, notaryParty)
|
||||
it.signatures
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateSignature(sig: TransactionSignature, txId: SecureHash, notaryParty: Party) {
|
||||
check(sig.by in notaryParty.owningKey.keys) { "Invalid signer for the notary result" }
|
||||
sig.verify(txId)
|
||||
}
|
||||
|
||||
/**
|
||||
* The [NotarySendTransactionFlow] flow is similar to [SendTransactionFlow], but uses [NotarisationPayload] as the
|
||||
* initial message, and retries message delivery.
|
||||
@ -166,11 +147,17 @@ class NotaryFlow {
|
||||
check(serviceHub.myInfo.legalIdentities.any { serviceHub.networkMapCache.isNotary(it) }) {
|
||||
"We are not a notary on the network"
|
||||
}
|
||||
val (id, inputs, timeWindow, notary) = receiveAndVerifyTx()
|
||||
checkNotary(notary)
|
||||
service.validateTimeWindow(timeWindow)
|
||||
service.commitInputStates(inputs, id, otherSideSession.counterparty)
|
||||
signAndSendResponse(id)
|
||||
var txId: SecureHash? = null
|
||||
try {
|
||||
val parts = receiveAndVerifyTx()
|
||||
txId = parts.id
|
||||
checkNotary(parts.notary)
|
||||
service.validateTimeWindow(parts.timestamp)
|
||||
service.commitInputStates(parts.inputs, txId, otherSideSession.counterparty)
|
||||
signTransactionAndSendResponse(txId)
|
||||
} catch (e: NotaryInternalException) {
|
||||
throw NotaryException(e.error, txId)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@ -185,14 +172,14 @@ class NotaryFlow {
|
||||
@Suspendable
|
||||
protected fun checkNotary(notary: Party?) {
|
||||
if (notary?.owningKey != service.notaryIdentityKey) {
|
||||
throw NotaryException(NotaryError.WrongNotary)
|
||||
throw NotaryInternalException(NotaryError.WrongNotary)
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun signAndSendResponse(txId: SecureHash) {
|
||||
private fun signTransactionAndSendResponse(txId: SecureHash) {
|
||||
val signature = service.sign(txId)
|
||||
otherSideSession.send(listOf(signature))
|
||||
otherSideSession.send(NotarisationResponse(listOf(signature)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -207,14 +194,27 @@ data class TransactionParts(val id: SecureHash, val inputs: List<StateRef>, val
|
||||
* Exception thrown by the notary service if any issues are encountered while trying to commit a transaction. The
|
||||
* underlying [error] specifies the cause of failure.
|
||||
*/
|
||||
class NotaryException(val error: NotaryError) : FlowException("Unable to notarise: $error")
|
||||
class NotaryException(
|
||||
/** Cause of notarisation failure. */
|
||||
val error: NotaryError,
|
||||
/** Id of the transaction to be notarised. Can be _null_ if an error occurred before the id could be resolved. */
|
||||
val txId: SecureHash? = null
|
||||
) : FlowException("Unable to notarise transaction${txId ?: " "}: $error")
|
||||
|
||||
/** Exception internal to the notary service. Does not get exposed to CorDapps and flows calling [NotaryFlow.Client]. */
|
||||
class NotaryInternalException(val error: NotaryError) : FlowException("Unable to notarise: $error")
|
||||
|
||||
/** Specifies the cause for notarisation request failure. */
|
||||
@CordaSerializable
|
||||
sealed class NotaryError {
|
||||
/** Occurs when one or more input states of transaction with [txId] have already been consumed by another transaction. */
|
||||
data class Conflict(val txId: SecureHash, val conflict: SignedData<UniquenessProvider.Conflict>) : NotaryError() {
|
||||
override fun toString() = "One or more input states for transaction $txId have been used in another transaction"
|
||||
/** Occurs when one or more input states have already been consumed by another transaction. */
|
||||
data class Conflict(
|
||||
/** Id of the transaction that was attempted to be notarised. */
|
||||
val txId: SecureHash,
|
||||
/** Specifies which states have already been consumed in another transaction. */
|
||||
val consumedStates: Map<StateRef, StateConsumptionDetails>
|
||||
) : NotaryError() {
|
||||
override fun toString() = "One or more input states have been used in another transaction"
|
||||
}
|
||||
|
||||
/** Occurs when time specified in the [TimeWindow] command is outside the allowed tolerance. */
|
||||
@ -246,3 +246,15 @@ sealed class NotaryError {
|
||||
override fun toString() = cause.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/** Contains information about the consuming transaction for a particular state. */
|
||||
// TODO: include notary timestamp?
|
||||
@CordaSerializable
|
||||
data class StateConsumptionDetails(
|
||||
/**
|
||||
* Hash of the consuming transaction id.
|
||||
*
|
||||
* Note that this is NOT the transaction id itself – revealing it could lead to privacy leaks.
|
||||
*/
|
||||
val hashOfTransactionId: SecureHash
|
||||
)
|
@ -34,9 +34,7 @@ import java.security.cert.X509Certificate
|
||||
// also note that IDs are numbered from 1 upwards, matching numbering of other enum types in ASN.1 specifications.
|
||||
// TODO: Link to the specification once it has a permanent URL
|
||||
enum class CertRole(val validParents: NonEmptySet<CertRole?>, val isIdentity: Boolean, val isWellKnown: Boolean) : ASN1Encodable {
|
||||
/**
|
||||
* Intermediate CA (Doorman service).
|
||||
*/
|
||||
/** Intermediate CA (Doorman service). */
|
||||
INTERMEDIATE_CA(NonEmptySet.of(null), false, false),
|
||||
/** Signing certificate for the network map. */
|
||||
NETWORK_MAP(NonEmptySet.of(null), false, false),
|
||||
@ -47,6 +45,10 @@ enum class CertRole(val validParents: NonEmptySet<CertRole?>, val isIdentity: Bo
|
||||
/** Transport layer security certificate for a node. */
|
||||
TLS(NonEmptySet.of(NODE_CA), false, false),
|
||||
/** Well known (publicly visible) identity of a legal entity. */
|
||||
// TODO: at the moment, Legal Identity certs are issued by Node CA only. However, [INTERMEDIATE_CA] is also added
|
||||
// as a valid parent of [LEGAL_IDENTITY] for backwards compatibility purposes (eg. if we decide TLS has its
|
||||
// own Root CA and Intermediate CA directly issues Legal Identities; thus, there won't be a requirement for
|
||||
// Node CA). Consider removing [INTERMEDIATE_CA] from [validParents] when the model is finalised.
|
||||
LEGAL_IDENTITY(NonEmptySet.of(INTERMEDIATE_CA, NODE_CA), true, true),
|
||||
/** Confidential (limited visibility) identity of a legal entity. */
|
||||
CONFIDENTIAL_LEGAL_IDENTITY(NonEmptySet.of(LEGAL_IDENTITY), true, false);
|
||||
|
@ -31,14 +31,14 @@ object ContractUpgradeUtils {
|
||||
val upgradedContractAttachmentId = getContractAttachmentId(upgradedContractClass.name, services)
|
||||
|
||||
val inputs = listOf(stateAndRef.ref)
|
||||
return ContractUpgradeWireTransaction(
|
||||
return ContractUpgradeTransactionBuilder(
|
||||
inputs,
|
||||
stateAndRef.state.notary,
|
||||
legacyContractAttachmentId,
|
||||
upgradedContractClass.name,
|
||||
upgradedContractAttachmentId,
|
||||
privacySalt
|
||||
)
|
||||
).build()
|
||||
}
|
||||
|
||||
private fun getContractAttachmentId(name: ContractClassName, services: ServicesForResolution): AttachmentId {
|
||||
|
16
core/src/main/kotlin/net/corda/core/internal/NotaryUtils.kt
Normal file
16
core/src/main/kotlin/net/corda/core/internal/NotaryUtils.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.flows.NotarisationResponse
|
||||
import net.corda.core.identity.Party
|
||||
|
||||
/**
|
||||
* Checks that there are sufficient signatures to satisfy the notary signing requirement and validates the signatures
|
||||
* against the given transaction id.
|
||||
*/
|
||||
fun NotarisationResponse.validateSignatures(txId: SecureHash, notary: Party) {
|
||||
val signingKeys = signatures.map { it.by }
|
||||
require(notary.owningKey.isFulfilledBy(signingKeys)) { "Insufficient signatures to fulfill the notary signing requirement for $notary" }
|
||||
signatures.forEach { it.verify(txId) }
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
/** Constructs a [NotaryChangeWireTransaction]. */
|
||||
class NotaryChangeTransactionBuilder(val inputs: List<StateRef>,
|
||||
val notary: Party,
|
||||
val newNotary: Party) {
|
||||
fun build(): NotaryChangeWireTransaction {
|
||||
val components = listOf(inputs, notary, newNotary).map { it.serialize() }
|
||||
return NotaryChangeWireTransaction(components)
|
||||
}
|
||||
}
|
||||
|
||||
/** Constructs a [ContractUpgradeWireTransaction]. */
|
||||
class ContractUpgradeTransactionBuilder(
|
||||
val inputs: List<StateRef>,
|
||||
val notary: Party,
|
||||
val legacyContractAttachmentId: SecureHash,
|
||||
val upgradedContractClassName: ContractClassName,
|
||||
val upgradedContractAttachmentId: SecureHash,
|
||||
val privacySalt: PrivacySalt = PrivacySalt()) {
|
||||
fun build(): ContractUpgradeWireTransaction {
|
||||
val components = listOf(inputs, notary, legacyContractAttachmentId, upgradedContractClassName, upgradedContractAttachmentId).map { it.serialize() }
|
||||
return ContractUpgradeWireTransaction(components, privacySalt)
|
||||
}
|
||||
}
|
||||
|
||||
/** Concatenates the hash components into a single [ByteArray] and returns its hash. */
|
||||
fun combinedHash(components: Iterable<SecureHash>): SecureHash {
|
||||
val stream = ByteArrayOutputStream()
|
||||
components.forEach {
|
||||
stream.write(it.bytes)
|
||||
}
|
||||
return stream.toByteArray().sha256()
|
||||
}
|
@ -18,7 +18,6 @@ import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import org.slf4j.Logger
|
||||
import java.security.PublicKey
|
||||
@ -40,18 +39,19 @@ abstract class NotaryService : SingletonSerializeAsToken() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current instant provided by the clock falls within the specified time window.
|
||||
* Checks if the current instant provided by the clock falls within the specified time window. Should only be
|
||||
* used by a notary service flow.
|
||||
*
|
||||
* @throws NotaryException if current time is outside the specified time window. The exception contains
|
||||
* @throws NotaryInternalException if current time is outside the specified time window. The exception contains
|
||||
* the [NotaryError.TimeWindowInvalid] error.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(NotaryException::class)
|
||||
@Throws(NotaryInternalException::class)
|
||||
fun validateTimeWindow(clock: Clock, timeWindow: TimeWindow?) {
|
||||
if (timeWindow == null) return
|
||||
val currentTime = clock.instant()
|
||||
if (currentTime !in timeWindow) {
|
||||
throw NotaryException(
|
||||
throw NotaryInternalException(
|
||||
NotaryError.TimeWindowInvalid(currentTime, timeWindow)
|
||||
)
|
||||
}
|
||||
@ -92,28 +92,24 @@ abstract class TrustedAuthorityNotaryService : NotaryService() {
|
||||
fun commitInputStates(inputs: List<StateRef>, txId: SecureHash, caller: Party) {
|
||||
try {
|
||||
uniquenessProvider.commit(inputs, txId, caller)
|
||||
} catch (e: UniquenessException) {
|
||||
val conflicts = inputs.filterIndexed { i, stateRef ->
|
||||
val consumingTx = e.error.stateHistory[stateRef]
|
||||
consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(txId, i, caller)
|
||||
} catch (e: NotaryInternalException) {
|
||||
if (e.error is NotaryError.Conflict) {
|
||||
val conflicts = inputs.filterIndexed { _, stateRef ->
|
||||
val cause = e.error.consumedStates[stateRef]
|
||||
cause != null && cause.hashOfTransactionId != txId.sha256()
|
||||
}
|
||||
if (conflicts.isNotEmpty()) {
|
||||
// TODO: Create a new UniquenessException that only contains the conflicts filtered above.
|
||||
log.warn("Notary conflicts for $txId: $conflicts")
|
||||
throw notaryException(txId, e)
|
||||
log.info("Notary conflicts for $txId: $conflicts")
|
||||
throw e
|
||||
}
|
||||
} else throw e
|
||||
} catch (e: Exception) {
|
||||
log.error("Internal error", e)
|
||||
throw NotaryException(NotaryError.General(Exception("Service unavailable, please try again later")))
|
||||
throw NotaryInternalException(NotaryError.General(Exception("Service unavailable, please try again later")))
|
||||
}
|
||||
}
|
||||
|
||||
private fun notaryException(txId: SecureHash, e: UniquenessException): NotaryException {
|
||||
val conflictData = e.error.serialize()
|
||||
val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
|
||||
return NotaryException(NotaryError.Conflict(txId, signedConflict))
|
||||
}
|
||||
|
||||
/** Sign a [ByteArray] input. */
|
||||
fun sign(bits: ByteArray): DigitalSignature.WithKey {
|
||||
return services.keyManagementService.sign(bits, notaryIdentityKey)
|
||||
@ -127,6 +123,8 @@ abstract class TrustedAuthorityNotaryService : NotaryService() {
|
||||
|
||||
// TODO: Sign multiple transactions at once by building their Merkle tree and then signing over its root.
|
||||
|
||||
@Deprecated("This property is no longer used") @Suppress("DEPRECATION")
|
||||
protected open val timeWindowChecker: TimeWindowChecker get() = throw UnsupportedOperationException("No default implementation, need to override")
|
||||
@Deprecated("This property is no longer used")
|
||||
@Suppress("DEPRECATION")
|
||||
protected open val timeWindowChecker: TimeWindowChecker
|
||||
get() = throw UnsupportedOperationException("No default implementation, need to override")
|
||||
}
|
@ -23,24 +23,22 @@ import net.corda.core.serialization.CordaSerializable
|
||||
* A uniqueness provider is expected to be used from within the context of a flow.
|
||||
*/
|
||||
interface UniquenessProvider {
|
||||
/** Commits all input states of the given transaction */
|
||||
/** Commits all input states of the given transaction. */
|
||||
fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party)
|
||||
|
||||
/** Specifies the consuming transaction for every conflicting state */
|
||||
/** Specifies the consuming transaction for every conflicting state. */
|
||||
@CordaSerializable
|
||||
@Deprecated("No longer used due to potential privacy leak")
|
||||
data class Conflict(val stateHistory: Map<StateRef, ConsumingTx>)
|
||||
|
||||
/**
|
||||
* Specifies the transaction id, the position of the consumed state in the inputs, and
|
||||
* the caller identity requesting the commit.
|
||||
*
|
||||
* TODO: need to do more design work to prevent privacy problems: knowing the id of a
|
||||
* transaction, by the rules of our system the party can obtain it and see its contents.
|
||||
* This allows a party to just submit invalid transactions with outputs it was aware of and
|
||||
* find out where exactly they were spent.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party)
|
||||
}
|
||||
|
||||
@Deprecated("No longer used due to potential privacy leak")
|
||||
@Suppress("DEPRECATION")
|
||||
class UniquenessException(val error: UniquenessProvider.Conflict) : CordaException(UniquenessException::class.java.name)
|
@ -16,13 +16,14 @@ import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.services.vault.PageSpecification
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.core.node.services.Vault.StateStatus
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import rx.Observable
|
||||
import java.time.Instant
|
||||
@ -120,15 +121,15 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
/**
|
||||
* Returned in queries [VaultService.queryBy] and [VaultService.trackBy].
|
||||
* A Page contains:
|
||||
* 1) a [List] of actual [StateAndRef] requested by the specified [QueryCriteria] to a maximum of [MAX_PAGE_SIZE]
|
||||
* 2) a [List] of associated [Vault.StateMetadata], one per [StateAndRef] result
|
||||
* 3) a total number of states that met the given [QueryCriteria] if a [PageSpecification] was provided
|
||||
* (otherwise defaults to -1)
|
||||
* 4) Status types used in this query: UNCONSUMED, CONSUMED, ALL
|
||||
* 5) Other results as a [List] of any type (eg. aggregate function results with/without group by)
|
||||
* 1) a [List] of actual [StateAndRef] requested by the specified [QueryCriteria] to a maximum of [MAX_PAGE_SIZE].
|
||||
* 2) a [List] of associated [Vault.StateMetadata], one per [StateAndRef] result.
|
||||
* 3) a total number of states that met the given [QueryCriteria] if a [PageSpecification] was provided,
|
||||
* otherwise it defaults to -1.
|
||||
* 4) Status types used in this query: [StateStatus.UNCONSUMED], [StateStatus.CONSUMED], [StateStatus.ALL].
|
||||
* 5) Other results as a [List] of any type (eg. aggregate function results with/without group by).
|
||||
*
|
||||
* Note: currently otherResults are used only for Aggregate Functions (in which case, the states and statesMetadata
|
||||
* results will be empty)
|
||||
* results will be empty).
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Page<out T : ContractState>(val states: List<StateAndRef<T>>,
|
||||
@ -168,17 +169,18 @@ interface VaultService {
|
||||
/**
|
||||
* Prefer the use of [updates] unless you know why you want to use this instead.
|
||||
*
|
||||
* Get a synchronous Observable of updates. When observations are pushed to the Observer, the Vault will already incorporate
|
||||
* the update, and the database transaction associated with the update will still be open and current. If for some
|
||||
* reason the processing crosses outside of the database transaction (for example, the update is pushed outside the current
|
||||
* JVM or across to another [Thread] which is executing in a different database transaction) then the Vault may
|
||||
* not incorporate the update due to racing with committing the current database transaction.
|
||||
* Get a synchronous [Observable] of updates. When observations are pushed to the Observer, the [Vault] will already
|
||||
* incorporate the update, and the database transaction associated with the update will still be open and current.
|
||||
* If for some reason the processing crosses outside of the database transaction (for example, the update is pushed
|
||||
* outside the current JVM or across to another [Thread], which is executing in a different database transaction),
|
||||
* then the [Vault] may not incorporate the update due to racing with committing the current database transaction.
|
||||
*/
|
||||
val rawUpdates: Observable<Vault.Update<ContractState>>
|
||||
|
||||
/**
|
||||
* Get a synchronous Observable of updates. When observations are pushed to the Observer, the Vault will already incorporate
|
||||
* the update, and the database transaction associated with the update will have been committed and closed.
|
||||
* Get a synchronous [Observable] of updates. When observations are pushed to the Observer, the [Vault] will
|
||||
* already incorporate the update and the database transaction associated with the update will have been committed
|
||||
* and closed.
|
||||
*/
|
||||
val updates: Observable<Vault.Update<ContractState>>
|
||||
|
||||
@ -190,10 +192,10 @@ interface VaultService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a note to an existing [LedgerTransaction] given by its unique [SecureHash] id
|
||||
* Add a note to an existing [LedgerTransaction] given by its unique [SecureHash] id.
|
||||
* Multiple notes may be attached to the same [LedgerTransaction].
|
||||
* These are additively and immutably persisted within the node local vault database in a single textual field
|
||||
* using a semi-colon separator
|
||||
* These are additively and immutably persisted within the node local vault database in a single textual field.
|
||||
* using a semi-colon separator.
|
||||
*/
|
||||
fun addNoteToTransaction(txnId: SecureHash, noteText: String)
|
||||
|
||||
@ -202,7 +204,7 @@ interface VaultService {
|
||||
// DOCEND VaultStatesQuery
|
||||
|
||||
/**
|
||||
* Soft locking is used to prevent multiple transactions trying to use the same output simultaneously.
|
||||
* Soft locking is used to prevent multiple transactions trying to use the same states simultaneously.
|
||||
* Violation of a soft lock would result in a double spend being created and rejected by the notary.
|
||||
*/
|
||||
|
||||
@ -210,35 +212,35 @@ interface VaultService {
|
||||
|
||||
/**
|
||||
* Reserve a set of [StateRef] for a given [UUID] unique identifier.
|
||||
* Typically, the unique identifier will refer to a [FlowLogic.runId.uuid] associated with an in-flight flow.
|
||||
* Typically, the unique identifier will refer to a [FlowLogic.runId]'s [UUID] associated with an in-flight flow.
|
||||
* In this case if the flow terminates the locks will automatically be freed, even if there is an error.
|
||||
* However, the user can specify their own [UUID] and manage this manually, possibly across the lifetime of multiple flows,
|
||||
* or from other thread contexts e.g. [CordaService] instances.
|
||||
* However, the user can specify their own [UUID] and manage this manually, possibly across the lifetime of multiple
|
||||
* flows, or from other thread contexts e.g. [CordaService] instances.
|
||||
* In the case of coin selection, soft locks are automatically taken upon gathering relevant unconsumed input refs.
|
||||
*
|
||||
* @throws [StatesNotAvailableException] when not possible to softLock all of requested [StateRef]
|
||||
* @throws [StatesNotAvailableException] when not possible to soft-lock all of requested [StateRef].
|
||||
*/
|
||||
@Throws(StatesNotAvailableException::class)
|
||||
fun softLockReserve(lockId: UUID, stateRefs: NonEmptySet<StateRef>)
|
||||
|
||||
/**
|
||||
* Release all or an explicitly specified set of [StateRef] for a given [UUID] unique identifier.
|
||||
* A vault soft lock manager is automatically notified of a Flows that are terminated, such that any soft locked states
|
||||
* may be released.
|
||||
* In the case of coin selection, softLock are automatically released once previously gathered unconsumed input refs
|
||||
* are consumed as part of cash spending.
|
||||
* A [Vault] soft-lock manager is automatically notified from flows that are terminated, such that any soft locked
|
||||
* states may be released.
|
||||
* In the case of coin selection, soft-locks are automatically released once previously gathered unconsumed
|
||||
* input refs are consumed as part of cash spending.
|
||||
*/
|
||||
fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>? = null)
|
||||
// DOCEND SoftLockAPI
|
||||
|
||||
/**
|
||||
* Helper function to determine spendable states and soft locking them.
|
||||
* Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending`
|
||||
* Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending`.
|
||||
* However, this is fully generic and can operate with custom [FungibleAsset] states.
|
||||
* @param lockId The [FlowLogic.runId.uuid] of the current flow used to soft lock the states.
|
||||
* @param lockId The [FlowLogic.runId]'s [UUID] of the current flow used to soft lock the states.
|
||||
* @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the
|
||||
* [contractStateType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the UNCONSUMED,
|
||||
* soft lock and contract type requirements.
|
||||
* [contractStateType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the
|
||||
* [StateStatus.UNCONSUMED], soft lock and contract type requirements.
|
||||
* @param amount The required amount of the asset, but with the issuer stripped off.
|
||||
* It is assumed that compatible issuer states will be filtered out by the [eligibleStatesQuery].
|
||||
* @param contractStateType class type of the result set.
|
||||
@ -259,12 +261,12 @@ interface VaultService {
|
||||
* and returns a [Vault.Page] object containing the following:
|
||||
* 1. states as a List of <StateAndRef> (page number and size defined by [PageSpecification])
|
||||
* 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table.
|
||||
* 3. total number of results available if [PageSpecification] supplied (otherwise returns -1)
|
||||
* 4. status types used in this query: UNCONSUMED, CONSUMED, ALL
|
||||
* 5. other results (aggregate functions with/without using value groups)
|
||||
* 3. total number of results available if [PageSpecification] supplied (otherwise returns -1).
|
||||
* 4. status types used in this query: [StateStatus.UNCONSUMED], [StateStatus.CONSUMED], [StateStatus.ALL].
|
||||
* 5. other results (aggregate functions with/without using value groups).
|
||||
*
|
||||
* @throws VaultQueryException if the query cannot be executed for any reason
|
||||
* (missing criteria or parsing error, paging errors, unsupported query, underlying database error)
|
||||
* (missing criteria or parsing error, paging errors, unsupported query, underlying database error).
|
||||
*
|
||||
* Notes
|
||||
* If no [PageSpecification] is provided, a maximum of [DEFAULT_PAGE_SIZE] results will be returned.
|
||||
@ -281,11 +283,11 @@ interface VaultService {
|
||||
/**
|
||||
* Generic vault query function which takes a [QueryCriteria] object to define filters,
|
||||
* optional [PageSpecification] and optional [Sort] modification criteria (default unsorted),
|
||||
* and returns a [Vault.PageAndUpdates] object containing
|
||||
* 1) a snapshot as a [Vault.Page] (described previously in [queryBy])
|
||||
* 2) an [Observable] of [Vault.Update]
|
||||
* and returns a [DataFeed] object containing:
|
||||
* 1) a snapshot as a [Vault.Page] (described previously in [queryBy]).
|
||||
* 2) an [Observable] of [Vault.Update].
|
||||
*
|
||||
* @throws VaultQueryException if the query cannot be executed for any reason
|
||||
* @throws VaultQueryException if the query cannot be executed for any reason.
|
||||
*
|
||||
* Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function.
|
||||
* the [QueryCriteria] applies to both snapshot and deltas (streaming updates).
|
||||
@ -297,8 +299,8 @@ interface VaultService {
|
||||
contractStateType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>>
|
||||
// DOCEND VaultQueryAPI
|
||||
|
||||
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
|
||||
// Java Helpers
|
||||
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations.
|
||||
// Java Helpers.
|
||||
fun <T : ContractState> queryBy(contractStateType: Class<out T>): Vault.Page<T> {
|
||||
return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
@ -13,12 +13,18 @@ package net.corda.core.transactions
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.serializedHash
|
||||
import net.corda.core.crypto.componentHash
|
||||
import net.corda.core.crypto.computeNonce
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.combinedHash
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.transactions.ContractUpgradeFilteredTransaction.FilteredComponent
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import java.security.PublicKey
|
||||
|
||||
@ -28,13 +34,20 @@ import java.security.PublicKey
|
||||
/** A special transaction for upgrading the contract of a state. */
|
||||
@CordaSerializable
|
||||
data class ContractUpgradeWireTransaction(
|
||||
override val inputs: List<StateRef>,
|
||||
override val notary: Party,
|
||||
val legacyContractAttachmentId: SecureHash,
|
||||
val upgradeContractClassName: ContractClassName,
|
||||
val upgradedContractAttachmentId: SecureHash,
|
||||
/**
|
||||
* Contains all of the transaction components in serialized form.
|
||||
* This is used for calculating the transaction id in a deterministic fashion, since re-serializing properties
|
||||
* may result in a different byte sequence depending on the serialization context.
|
||||
*/
|
||||
val serializedComponents: List<OpaqueBytes>,
|
||||
/** Required for hiding components in [ContractUpgradeFilteredTransaction]. */
|
||||
val privacySalt: PrivacySalt = PrivacySalt()
|
||||
) : CoreTransaction() {
|
||||
override val inputs: List<StateRef> = serializedComponents[INPUTS.ordinal].deserialize()
|
||||
override val notary: Party by lazy { serializedComponents[NOTARY.ordinal].deserialize<Party>() }
|
||||
val legacyContractAttachmentId: SecureHash by lazy { serializedComponents[LEGACY_ATTACHMENT.ordinal].deserialize<SecureHash>() }
|
||||
val upgradedContractClassName: ContractClassName by lazy { serializedComponents[UPGRADED_CONTRACT.ordinal].deserialize<ContractClassName>() }
|
||||
val upgradedContractAttachmentId: SecureHash by lazy { serializedComponents[UPGRADED_ATTACHMENT.ordinal].deserialize<SecureHash>() }
|
||||
|
||||
init {
|
||||
check(inputs.isNotEmpty()) { "A contract upgrade transaction must have inputs" }
|
||||
@ -49,11 +62,17 @@ data class ContractUpgradeWireTransaction(
|
||||
get() = throw UnsupportedOperationException("ContractUpgradeWireTransaction does not contain output states, " +
|
||||
"outputs can only be obtained from a resolved ContractUpgradeLedgerTransaction")
|
||||
|
||||
/** Hash of the list of components that are hidden in the [ContractUpgradeFilteredTransaction]. */
|
||||
private val hiddenComponentHash: SecureHash
|
||||
get() = serializedHash(listOf(legacyContractAttachmentId, upgradeContractClassName, privacySalt))
|
||||
override val id: SecureHash by lazy {
|
||||
val componentHashes =serializedComponents.mapIndexed { index, component ->
|
||||
componentHash(nonces[index], component)
|
||||
}
|
||||
combinedHash(componentHashes)
|
||||
}
|
||||
|
||||
override val id: SecureHash by lazy { serializedHash(inputs + notary).hashConcat(hiddenComponentHash) }
|
||||
/** Required for filtering transaction components. */
|
||||
private val nonces = (0 until serializedComponents.size).map {
|
||||
computeNonce(privacySalt, it, 0)
|
||||
}
|
||||
|
||||
/** Resolves input states and contract attachments, and builds a ContractUpgradeLedgerTransaction. */
|
||||
fun resolve(services: ServicesForResolution, sigs: List<TransactionSignature>): ContractUpgradeLedgerTransaction {
|
||||
@ -66,7 +85,7 @@ data class ContractUpgradeWireTransaction(
|
||||
resolvedInputs,
|
||||
notary,
|
||||
legacyContractAttachment,
|
||||
upgradeContractClassName,
|
||||
upgradedContractClassName,
|
||||
upgradedContractAttachment,
|
||||
id,
|
||||
privacySalt,
|
||||
@ -75,8 +94,23 @@ data class ContractUpgradeWireTransaction(
|
||||
)
|
||||
}
|
||||
|
||||
/** Constructs a filtered transaction: the inputs and the notary party are always visible, while the rest are hidden. */
|
||||
fun buildFilteredTransaction(): ContractUpgradeFilteredTransaction {
|
||||
return ContractUpgradeFilteredTransaction(inputs, notary, hiddenComponentHash)
|
||||
val totalComponents = (0 until serializedComponents.size).toSet()
|
||||
val visibleComponents = mapOf(
|
||||
INPUTS.ordinal to FilteredComponent(serializedComponents[INPUTS.ordinal], nonces[INPUTS.ordinal]),
|
||||
NOTARY.ordinal to FilteredComponent(serializedComponents[NOTARY.ordinal], nonces[NOTARY.ordinal])
|
||||
)
|
||||
val hiddenComponents = (totalComponents - visibleComponents.keys).map { index ->
|
||||
val hash = componentHash(nonces[index], serializedComponents[index])
|
||||
index to hash
|
||||
}.toMap()
|
||||
|
||||
return ContractUpgradeFilteredTransaction(visibleComponents, hiddenComponents)
|
||||
}
|
||||
|
||||
enum class Component {
|
||||
INPUTS, NOTARY, LEGACY_ATTACHMENT, UPGRADED_CONTRACT, UPGRADED_ATTACHMENT
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,19 +118,43 @@ data class ContractUpgradeWireTransaction(
|
||||
* A filtered version of the [ContractUpgradeWireTransaction]. In comparison with a regular [FilteredTransaction], there
|
||||
* is no flexibility on what parts of the transaction to reveal – the inputs and notary field are always visible and the
|
||||
* rest of the transaction is always hidden. Its only purpose is to hide transaction data when using a non-validating notary.
|
||||
*
|
||||
* @property inputs The inputs of this transaction.
|
||||
* @property notary The notary for this transaction.
|
||||
* @property rest Hash of the hidden components of the [ContractUpgradeWireTransaction].
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class ContractUpgradeFilteredTransaction(
|
||||
override val inputs: List<StateRef>,
|
||||
override val notary: Party,
|
||||
val rest: SecureHash
|
||||
/** Transaction components that are exposed. */
|
||||
val visibleComponents: Map<Int, FilteredComponent>,
|
||||
/**
|
||||
* Hashes of the transaction components that are not revealed in this transaction.
|
||||
* Required for computing the transaction id.
|
||||
*/
|
||||
val hiddenComponents: Map<Int, SecureHash>
|
||||
) : CoreTransaction() {
|
||||
override val id: SecureHash get() = serializedHash(inputs + notary).hashConcat(rest)
|
||||
override val inputs: List<StateRef> by lazy {
|
||||
visibleComponents[INPUTS.ordinal]?.component?.deserialize<List<StateRef>>()
|
||||
?: throw IllegalArgumentException("Inputs not specified")
|
||||
}
|
||||
override val notary: Party by lazy {
|
||||
visibleComponents[NOTARY.ordinal]?.component?.deserialize<Party>()
|
||||
?: throw IllegalArgumentException("Notary not specified")
|
||||
}
|
||||
override val id: SecureHash by lazy {
|
||||
val totalComponents = visibleComponents.size + hiddenComponents.size
|
||||
val hashList = (0 until totalComponents).map { i ->
|
||||
when {
|
||||
visibleComponents.containsKey(i) -> {
|
||||
componentHash(visibleComponents[i]!!.nonce, visibleComponents[i]!!.component)
|
||||
}
|
||||
hiddenComponents.containsKey(i) -> hiddenComponents[i]!!
|
||||
else -> throw IllegalStateException("Missing component hashes")
|
||||
}
|
||||
}
|
||||
combinedHash(hashList)
|
||||
}
|
||||
override val outputs: List<TransactionState<ContractState>> get() = emptyList()
|
||||
|
||||
/** Contains the serialized component and the associated nonce for computing the transaction id. */
|
||||
@CordaSerializable
|
||||
class FilteredComponent(val component: OpaqueBytes, val nonce: SecureHash)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,7 +171,7 @@ data class ContractUpgradeLedgerTransaction(
|
||||
override val inputs: List<StateAndRef<ContractState>>,
|
||||
override val notary: Party,
|
||||
val legacyContractAttachment: Attachment,
|
||||
val upgradeContractClassName: ContractClassName,
|
||||
val upgradedContractClassName: ContractClassName,
|
||||
val upgradedContractAttachment: Attachment,
|
||||
override val id: SecureHash,
|
||||
val privacySalt: PrivacySalt,
|
||||
@ -175,7 +233,7 @@ data class ContractUpgradeLedgerTransaction(
|
||||
// TODO: re-map encumbrance pointers
|
||||
input.state.copy(
|
||||
data = upgradedState,
|
||||
contract = upgradeContractClassName,
|
||||
contract = upgradedContractClassName,
|
||||
constraint = outputConstraint
|
||||
)
|
||||
}
|
||||
@ -192,7 +250,7 @@ data class ContractUpgradeLedgerTransaction(
|
||||
private fun loadUpgradedContract(): UpgradedContract<ContractState, *> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return this::class.java.classLoader
|
||||
.loadClass(upgradeContractClassName)
|
||||
.loadClass(upgradedContractClassName)
|
||||
.asSubclass(Contract::class.java)
|
||||
.getConstructor()
|
||||
.newInstance() as UpgradedContract<ContractState, *>
|
||||
|
@ -410,6 +410,6 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
notary: Party?,
|
||||
timeWindow: TimeWindow?,
|
||||
privacySalt: PrivacySalt
|
||||
) = copy(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null)
|
||||
) = copy(inputs = inputs, outputs = outputs, commands = commands, attachments = attachments, id = id, notary = notary, timeWindow = timeWindow, privacySalt = privacySalt, networkParameters = null)
|
||||
}
|
||||
|
||||
|
@ -95,18 +95,18 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
|
||||
val signersList = deserialiseComponentGroup(ComponentGroupEnum.SIGNERS_GROUP, { SerializedBytes<List<PublicKey>>(it).deserialize() })
|
||||
val commandDataList = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes<CommandData>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
|
||||
val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal }
|
||||
if (group is FilteredComponentGroup) {
|
||||
return if (group is FilteredComponentGroup) {
|
||||
check(commandDataList.size <= signersList.size) { "Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects" }
|
||||
val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }
|
||||
val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) }
|
||||
if (leafIndices.isNotEmpty())
|
||||
check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" }
|
||||
return commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[leafIndices[index]]) }
|
||||
commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[leafIndices[index]]) }
|
||||
} else {
|
||||
// It is a WireTransaction
|
||||
// or a FilteredTransaction with no Commands (in which case group is null).
|
||||
check(commandDataList.size == signersList.size) { "Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match" }
|
||||
return commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[index]) }
|
||||
commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[index]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -158,9 +158,9 @@ class FilteredTransaction internal constructor(
|
||||
// As all of the helper Map structures, like availableComponentNonces, availableComponentHashes
|
||||
// and groupsMerkleRoots, are computed lazily via componentGroups.forEach, there should always be
|
||||
// a match on Map.get ensuring it will never return null.
|
||||
filteredSerialisedComponents.put(componentGroupIndex, mutableListOf(serialisedComponent))
|
||||
filteredComponentNonces.put(componentGroupIndex, mutableListOf(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]))
|
||||
filteredComponentHashes.put(componentGroupIndex, mutableListOf(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]))
|
||||
filteredSerialisedComponents[componentGroupIndex] = mutableListOf(serialisedComponent)
|
||||
filteredComponentNonces[componentGroupIndex] = mutableListOf(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex])
|
||||
filteredComponentHashes[componentGroupIndex] = mutableListOf(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex])
|
||||
} else {
|
||||
group.add(serialisedComponent)
|
||||
// If the group[componentGroupIndex] existed, then we guarantee that
|
||||
@ -175,9 +175,9 @@ class FilteredTransaction internal constructor(
|
||||
val signersGroupIndex = ComponentGroupEnum.SIGNERS_GROUP.ordinal
|
||||
// There exist commands, thus the signers group is not empty.
|
||||
val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex }
|
||||
filteredSerialisedComponents.put(signersGroupIndex, signersGroupComponents.components.toMutableList())
|
||||
filteredComponentNonces.put(signersGroupIndex, wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList())
|
||||
filteredComponentHashes.put(signersGroupIndex, wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList())
|
||||
filteredSerialisedComponents[signersGroupIndex] = signersGroupComponents.components.toMutableList()
|
||||
filteredComponentNonces[signersGroupIndex] = wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList()
|
||||
filteredComponentHashes[signersGroupIndex] = wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -322,14 +322,14 @@ class FilteredTransaction internal constructor(
|
||||
.filter { signers -> publicKey in signers }.size
|
||||
}
|
||||
|
||||
inline private fun verificationCheck(value: Boolean, lazyMessage: () -> Any) {
|
||||
private inline fun verificationCheck(value: Boolean, lazyMessage: () -> Any) {
|
||||
if (!value) {
|
||||
val message = lazyMessage()
|
||||
throw FilteredTransactionVerificationException(id, message.toString())
|
||||
}
|
||||
}
|
||||
|
||||
inline private fun visibilityCheck(value: Boolean, lazyMessage: () -> Any) {
|
||||
private inline fun visibilityCheck(value: Boolean, lazyMessage: () -> Any) {
|
||||
if (!value) {
|
||||
val message = lazyMessage()
|
||||
throw ComponentVisibilityException(id, message.toString())
|
||||
|
@ -13,11 +13,15 @@ package net.corda.core.transactions
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.serializedHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction.Component.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import java.security.PublicKey
|
||||
|
||||
@ -28,10 +32,18 @@ import java.security.PublicKey
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class NotaryChangeWireTransaction(
|
||||
override val inputs: List<StateRef>,
|
||||
override val notary: Party,
|
||||
val newNotary: Party
|
||||
/**
|
||||
* Contains all of the transaction components in serialized form.
|
||||
* This is used for calculating the transaction id in a deterministic fashion, since re-serializing properties
|
||||
* may result in a different byte sequence depending on the serialization context.
|
||||
*/
|
||||
val serializedComponents: List<OpaqueBytes>
|
||||
) : CoreTransaction() {
|
||||
override val inputs: List<StateRef> = serializedComponents[INPUTS.ordinal].deserialize()
|
||||
override val notary: Party = serializedComponents[NOTARY.ordinal].deserialize()
|
||||
/** Identity of the notary service to reassign the states to.*/
|
||||
val newNotary: Party = serializedComponents[NEW_NOTARY.ordinal].deserialize()
|
||||
|
||||
/**
|
||||
* This transaction does not contain any output states, outputs can be obtained by resolving a
|
||||
* [NotaryChangeLedgerTransaction] and applying the notary modification to inputs.
|
||||
@ -49,16 +61,29 @@ data class NotaryChangeWireTransaction(
|
||||
* A privacy salt is not really required in this case, because we already used nonces in normal transactions and
|
||||
* thus input state refs will always be unique. Also, filtering doesn't apply on this type of transactions.
|
||||
*/
|
||||
override val id: SecureHash by lazy { serializedHash(inputs + notary + newNotary) }
|
||||
override val id: SecureHash by lazy {
|
||||
serializedComponents.map { component ->
|
||||
component.bytes.sha256()
|
||||
}.reduce { combinedHash, componentHash ->
|
||||
combinedHash.hashConcat(componentHash)
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */
|
||||
fun resolve(services: ServicesForResolution, sigs: List<TransactionSignature>) : NotaryChangeLedgerTransaction {
|
||||
fun resolve(services: ServicesForResolution, sigs: List<TransactionSignature>): NotaryChangeLedgerTransaction {
|
||||
val resolvedInputs = services.loadStates(inputs.toSet()).toList()
|
||||
return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs)
|
||||
}
|
||||
|
||||
/** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */
|
||||
fun resolve(services: ServiceHub, sigs: List<TransactionSignature>) = resolve(services as ServicesForResolution, sigs)
|
||||
|
||||
enum class Component {
|
||||
INPUTS, NOTARY, NEW_NOTARY
|
||||
}
|
||||
|
||||
@Deprecated("Required only for backwards compatibility purposes. This type of transaction should not be constructed outside Corda code.", ReplaceWith("NotaryChangeTransactionBuilder"), DeprecationLevel.WARNING)
|
||||
constructor(inputs: List<StateRef>, notary: Party, newNotary: Party) : this(listOf(inputs, notary, newNotary).map { it.serialize() })
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -112,7 +112,7 @@ open class TransactionBuilder(
|
||||
// with an explicit [AttachmentConstraint]
|
||||
val resolvedOutputs = outputs.map { state ->
|
||||
when {
|
||||
state.constraint !is AutomaticHashConstraint -> state
|
||||
state.constraint !== AutomaticHashConstraint -> state
|
||||
useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) -> state.copy(constraint = WhitelistedByZoneAttachmentConstraint)
|
||||
else -> services.cordappProvider.getContractAttachmentID(state.contract)?.let {
|
||||
state.copy(constraint = HashAttachmentConstraint(it))
|
||||
|
@ -79,11 +79,11 @@ The Demo Nodes can be started in one of three modes:
|
||||
|
||||
.. note:: 5 Corda nodes will be created on the following port on localhost by default.
|
||||
|
||||
* Notary -> 20001 (Does not accept logins)
|
||||
* Alice -> 20004
|
||||
* Bob -> 20007
|
||||
* UK Bank Plc -> 20010 (*Issuer node*)
|
||||
* USA Bank Corp -> 20013 (*Issuer node*)
|
||||
* Notary -> 20005 (Does not accept logins)
|
||||
* UK Bank Plc -> 20011 (*Issuer node*)
|
||||
* USA Bank Corp -> 20008 (*Issuer node*)
|
||||
* Alice -> 20017
|
||||
* Bob -> 20014
|
||||
|
||||
Explorer login credentials to the Issuer nodes are defaulted to ``manager`` and ``test``.
|
||||
Explorer login credentials to the Participants nodes are defaulted to ``user1`` and ``test``.
|
||||
|
@ -3,9 +3,10 @@
|
||||
Writing a custom notary service (experimental)
|
||||
==============================================
|
||||
|
||||
.. warning:: Customising a notary service is still an experimental feature and not recommended for most use-cases. Currently,
|
||||
customising Raft or BFT notaries is not yet fully supported. If you want to write your own Raft notary you will have to
|
||||
implement a custom database connector (or use a separate database for the notary), and use a custom configuration file.
|
||||
.. warning:: Customising a notary service is still an experimental feature and not recommended for most use-cases. The APIs
|
||||
for writing a custom notary may change in the future. Additionally, customising Raft or BFT notaries is not yet
|
||||
fully supported. If you want to write your own Raft notary you will have to implement a custom database connector
|
||||
(or use a separate database for the notary), and use a custom configuration file.
|
||||
|
||||
Similarly to writing an oracle service, the first step is to create a service class in your CorDapp and annotate it
|
||||
with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The custom notary
|
||||
|
@ -27,26 +27,37 @@ sealed class ConnectionDirection {
|
||||
) : ConnectionDirection()
|
||||
}
|
||||
|
||||
/** Class to set Artemis TCP configuration options. */
|
||||
class ArtemisTcpTransport {
|
||||
companion object {
|
||||
const val VERIFY_PEER_LEGAL_NAME = "corda.verifyPeerCommonName"
|
||||
|
||||
// Restrict enabled TLS cipher suites to:
|
||||
// AES128 using Galois/Counter Mode (GCM) for the block cipher being used to encrypt the message stream.
|
||||
// SHA256 as message authentication algorithm.
|
||||
// ECDHE as key exchange algorithm. DHE is also supported if one wants to completely avoid the use of ECC for TLS.
|
||||
// ECDSA and RSA for digital signatures. Our self-generated certificates all use ECDSA for handshakes,
|
||||
// but we allow classical RSA certificates to work in case:
|
||||
// a) we need to use keytool certificates in some demos,
|
||||
// b) we use cloud providers or HSMs that do not support ECC.
|
||||
/**
|
||||
* Corda supported TLS schemes.
|
||||
* <p><ul>
|
||||
* <li>TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
* <li>TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||
* <li>TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
|
||||
* </ul></p>
|
||||
* As shown above, current version restricts enabled TLS cipher suites to:
|
||||
* AES128 using Galois/Counter Mode (GCM) for the block cipher being used to encrypt the message stream.
|
||||
* SHA256 as message authentication algorithm.
|
||||
* Ephemeral Diffie Hellman key exchange for advanced forward secrecy. ECDHE is preferred, but DHE is also
|
||||
* supported in case one wants to completely avoid the use of ECC for TLS.
|
||||
* ECDSA and RSA for digital signatures. Our self-generated certificates all use ECDSA for handshakes,
|
||||
* but we allow classical RSA certificates to work in case one uses external tools or cloud providers or HSMs
|
||||
* that do not support ECC certificates.
|
||||
*/
|
||||
val CIPHER_SUITES = listOf(
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
|
||||
)
|
||||
|
||||
/** Supported TLS versions, currently TLSv1.2 only. */
|
||||
val TLS_VERSIONS = listOf("TLSv1.2")
|
||||
|
||||
/** Specify [TransportConfiguration] for TCP communication. */
|
||||
fun tcpTransport(
|
||||
direction: ConnectionDirection,
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
|
@ -41,13 +41,6 @@ class ArtemisMessagingComponent {
|
||||
const val BRIDGE_NOTIFY = "${INTERNAL_PREFIX}bridge.notify"
|
||||
const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications"
|
||||
|
||||
/**
|
||||
* In the operation mode where we have an out of process bridge we cannot correctly populate the Artemis validated user header
|
||||
* as the TLS does not terminate directly onto Artemis. We therefore use this internal only header to forward
|
||||
* the equivalent information from the Float.
|
||||
*/
|
||||
val bridgedCertificateSubject = SimpleString("sender-subject-name")
|
||||
|
||||
object P2PMessagingHeaders {
|
||||
// This is a "property" attached to an Artemis MQ message object, which contains our own notion of "topic".
|
||||
// We should probably try to unify our notion of "topic" (really, just a string that identifies an endpoint
|
||||
|
@ -54,6 +54,10 @@ data class ParametersUpdate(
|
||||
val updateDeadline: Instant
|
||||
)
|
||||
|
||||
/** Verify that a Network Map certificate is issued by Root CA and its [CertRole] is correct. */
|
||||
// TODO: Current implementation works under the assumption that there are no intermediate CAs between Root and
|
||||
// Network Map. Consider a more flexible implementation without the above assumption.
|
||||
|
||||
fun <T : Any> SignedDataWithCert<T>.verifiedNetworkMapCert(rootCert: X509Certificate): T {
|
||||
require(CertRole.extract(sig.by) == CertRole.NETWORK_MAP) { "Incorrect cert role: ${CertRole.extract(sig.by)}" }
|
||||
X509Utilities.validateCertificateChain(rootCert, sig.by, rootCert)
|
||||
|
@ -44,7 +44,12 @@ class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() {
|
||||
// is: https://youtrack.jetbrains.com/issue/KT-13077
|
||||
// TODO: Revisit this when Kotlin issue is fixed.
|
||||
|
||||
loggerFor<PropertySerializer>().error("Unexpected internal Kotlin error", e)
|
||||
// So this used to report as an error, but given we serialise exceptions all the time it
|
||||
// provides for very scary log files so move this to trace level
|
||||
loggerFor<PropertySerializer>().let { logger ->
|
||||
logger.trace("Using kotlin introspection on internal type ${this.declaringClass}")
|
||||
logger.trace("Unexpected internal Kotlin error", e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -80,7 +85,13 @@ class PrivatePropertyReader(val field: Field, parentType: Type) : PropertyReader
|
||||
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue
|
||||
// is: https://youtrack.jetbrains.com/issue/KT-13077
|
||||
// TODO: Revisit this when Kotlin issue is fixed.
|
||||
loggerFor<PropertySerializer>().error("Unexpected internal Kotlin error", e)
|
||||
|
||||
// So this used to report as an error, but given we serialise exceptions all the time it
|
||||
// provides for very scary log files so move this to trace level
|
||||
loggerFor<PropertySerializer>().let { logger ->
|
||||
logger.trace("Using kotlin introspection on internal type ${field}")
|
||||
logger.trace("Unexpected internal Kotlin error", e)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -32,10 +32,7 @@ import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.transactions.*
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.toNonEmptySet
|
||||
import net.corda.nodeapi.internal.serialization.CordaClassResolver
|
||||
@ -139,6 +136,7 @@ object DefaultKryoCustomizer {
|
||||
register(java.lang.invoke.SerializedLambda::class.java)
|
||||
register(ClosureSerializer.Closure::class.java, CordaClosureBlacklistSerializer)
|
||||
register(ContractUpgradeWireTransaction::class.java, ContractUpgradeWireTransactionSerializer)
|
||||
register(ContractUpgradeFilteredTransaction::class.java, ContractUpgradeFilteredTransactionSerializer)
|
||||
|
||||
for (whitelistProvider in serializationWhitelists) {
|
||||
val types = whitelistProvider.whitelist
|
||||
|
@ -18,14 +18,10 @@ import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer
|
||||
import com.esotericsoftware.kryo.serializers.FieldSerializer
|
||||
import com.esotericsoftware.kryo.util.MapReferenceResolver
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationContext.UseCase.Checkpoint
|
||||
@ -35,6 +31,7 @@ import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.toObservable
|
||||
import net.corda.core.transactions.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.core.utilities.SgxSupport
|
||||
import net.corda.nodeapi.internal.serialization.CordaClassResolver
|
||||
@ -268,40 +265,41 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
|
||||
@ThreadSafe
|
||||
object NotaryChangeWireTransactionSerializer : Serializer<NotaryChangeWireTransaction>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: NotaryChangeWireTransaction) {
|
||||
kryo.writeClassAndObject(output, obj.inputs)
|
||||
kryo.writeClassAndObject(output, obj.notary)
|
||||
kryo.writeClassAndObject(output, obj.newNotary)
|
||||
kryo.writeClassAndObject(output, obj.serializedComponents)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<NotaryChangeWireTransaction>): NotaryChangeWireTransaction {
|
||||
val inputs: List<StateRef> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
val notary = kryo.readClassAndObject(input) as Party
|
||||
val newNotary = kryo.readClassAndObject(input) as Party
|
||||
|
||||
return NotaryChangeWireTransaction(inputs, notary, newNotary)
|
||||
val components : List<OpaqueBytes> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
return NotaryChangeWireTransaction(components)
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
object ContractUpgradeWireTransactionSerializer : Serializer<ContractUpgradeWireTransaction>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: ContractUpgradeWireTransaction) {
|
||||
kryo.writeClassAndObject(output, obj.inputs)
|
||||
kryo.writeClassAndObject(output, obj.notary)
|
||||
kryo.writeClassAndObject(output, obj.legacyContractAttachmentId)
|
||||
kryo.writeClassAndObject(output, obj.upgradeContractClassName)
|
||||
kryo.writeClassAndObject(output, obj.upgradedContractAttachmentId)
|
||||
kryo.writeClassAndObject(output, obj.serializedComponents)
|
||||
kryo.writeClassAndObject(output, obj.privacySalt)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<ContractUpgradeWireTransaction>): ContractUpgradeWireTransaction {
|
||||
val inputs: List<StateRef> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
val notary = kryo.readClassAndObject(input) as Party
|
||||
val legacyContractAttachment = kryo.readClassAndObject(input) as SecureHash
|
||||
val upgradeContractClassName = kryo.readClassAndObject(input) as String
|
||||
val upgradedContractAttachment = kryo.readClassAndObject(input) as SecureHash
|
||||
val components: List<OpaqueBytes> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
val privacySalt = kryo.readClassAndObject(input) as PrivacySalt
|
||||
|
||||
return ContractUpgradeWireTransaction(inputs, notary, legacyContractAttachment, upgradeContractClassName, upgradedContractAttachment, privacySalt)
|
||||
return ContractUpgradeWireTransaction(components, privacySalt)
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
object ContractUpgradeFilteredTransactionSerializer : Serializer<ContractUpgradeFilteredTransaction>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: ContractUpgradeFilteredTransaction) {
|
||||
kryo.writeClassAndObject(output, obj.visibleComponents)
|
||||
kryo.writeClassAndObject(output, obj.hiddenComponents)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<ContractUpgradeFilteredTransaction>): ContractUpgradeFilteredTransaction {
|
||||
val visibleComponents: Map<Int, ContractUpgradeFilteredTransaction.FilteredComponent> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
val hiddenComponents: Map<Int, SecureHash> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
return ContractUpgradeFilteredTransaction(visibleComponents, hiddenComponents)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ package net.corda.nodeapi.internal.serialization.amqp
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.client.rpc.RPCException
|
||||
import net.corda.core.CordaException
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -1187,5 +1188,13 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi
|
||||
PrivateAckWrapper.serialize()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun throwable() {
|
||||
class TestException(message: String?, cause: Throwable?) : CordaException(message, cause)
|
||||
val testExcp = TestException("hello", Throwable().apply { stackTrace = Thread.currentThread().stackTrace } )
|
||||
val factory = testDefaultFactoryNoEvolution()
|
||||
SerializationOutput(factory).serialize(testExcp)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
@ -162,13 +163,12 @@ class BFTNotaryServiceTests : IntegrationTest() {
|
||||
}.single()
|
||||
spendTxs.zip(results).forEach { (tx, result) ->
|
||||
if (result is Try.Failure) {
|
||||
val error = (result.exception as NotaryException).error as NotaryError.Conflict
|
||||
val exception = result.exception as NotaryException
|
||||
val error = exception.error as NotaryError.Conflict
|
||||
assertEquals(tx.id, error.txId)
|
||||
val (stateRef, consumingTx) = error.conflict.verified().stateHistory.entries.single()
|
||||
val (stateRef, cause) = error.consumedStates.entries.single()
|
||||
assertEquals(StateRef(issueTx.id, 0), stateRef)
|
||||
assertEquals(spendTxs[successfulIndex].id, consumingTx.id)
|
||||
assertEquals(0, consumingTx.inputIndex)
|
||||
assertEquals(info.singleIdentity(), consumingTx.requestingParty)
|
||||
assertEquals(spendTxs[successfulIndex].id.sha256(), cause.hashOfTransactionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,16 +14,11 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.node.services.NotaryService
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
@ -91,18 +86,22 @@ class BFTNonValidatingNotaryService(
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
val payload = otherSideSession.receive<NotarisationPayload>().unwrap { it }
|
||||
val signatures = commit(payload)
|
||||
otherSideSession.send(signatures)
|
||||
val response = commit(payload)
|
||||
otherSideSession.send(response)
|
||||
return null
|
||||
}
|
||||
|
||||
private fun commit(payload: NotarisationPayload): List<DigitalSignature> {
|
||||
private fun commit(payload: NotarisationPayload): NotarisationResponse {
|
||||
val response = service.commitTransaction(payload, otherSideSession.counterparty)
|
||||
when (response) {
|
||||
is BFTSMaRt.ClusterResponse.Error -> throw NotaryException(response.error)
|
||||
is BFTSMaRt.ClusterResponse.Error -> {
|
||||
// TODO: here we assume that all error will be the same, but there might be invalid onces from mailicious nodes
|
||||
val responseError = response.errors.first().verified()
|
||||
throw NotaryException(responseError, payload.coreTransaction.id)
|
||||
}
|
||||
is BFTSMaRt.ClusterResponse.Signatures -> {
|
||||
log.debug("All input states of transaction ${payload.coreTransaction.id} have been committed")
|
||||
return response.txSignatures
|
||||
return NotarisationResponse(response.txSignatures)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -160,13 +159,16 @@ class BFTNonValidatingNotaryService(
|
||||
val inputs = transaction.inputs
|
||||
val notary = transaction.notary
|
||||
if (transaction is FilteredTransaction) NotaryService.validateTimeWindow(services.clock, transaction.timeWindow)
|
||||
if (notary !in services.myInfo.legalIdentities) throw NotaryException(NotaryError.WrongNotary)
|
||||
if (notary !in services.myInfo.legalIdentities) throw NotaryInternalException(NotaryError.WrongNotary)
|
||||
commitInputStates(inputs, id, callerIdentity)
|
||||
log.debug { "Inputs committed successfully, signing $id" }
|
||||
BFTSMaRt.ReplicaResponse.Signature(sign(id))
|
||||
} catch (e: NotaryException) {
|
||||
} catch (e: NotaryInternalException) {
|
||||
log.debug { "Error processing transaction: ${e.error}" }
|
||||
BFTSMaRt.ReplicaResponse.Error(e.error)
|
||||
val serializedError = e.error.serialize()
|
||||
val errorSignature = sign(serializedError.bytes)
|
||||
val signedError = SignedData(serializedError, errorSignature)
|
||||
BFTSMaRt.ReplicaResponse.Error(signedError)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,10 +24,8 @@ import bftsmart.tom.server.defaultservices.DefaultReplier
|
||||
import bftsmart.tom.util.Extractor
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.internal.declaredField
|
||||
import net.corda.core.internal.toTypedArray
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
@ -66,15 +64,15 @@ object BFTSMaRt {
|
||||
/** Sent from [Replica] to [Client]. */
|
||||
@CordaSerializable
|
||||
sealed class ReplicaResponse {
|
||||
data class Error(val error: NotaryError) : ReplicaResponse()
|
||||
data class Signature(val txSignature: DigitalSignature) : ReplicaResponse()
|
||||
data class Error(val error: SignedData<NotaryError>) : ReplicaResponse()
|
||||
data class Signature(val txSignature: TransactionSignature) : ReplicaResponse()
|
||||
}
|
||||
|
||||
/** An aggregate response from all replica ([Replica]) replies sent from [Client] back to the calling application. */
|
||||
@CordaSerializable
|
||||
sealed class ClusterResponse {
|
||||
data class Error(val error: NotaryError) : ClusterResponse()
|
||||
data class Signatures(val txSignatures: List<DigitalSignature>) : ClusterResponse()
|
||||
data class Error(val errors: List<SignedData<NotaryError>>) : ClusterResponse()
|
||||
data class Signatures(val txSignatures: List<TransactionSignature>) : ClusterResponse()
|
||||
}
|
||||
|
||||
interface Cluster {
|
||||
@ -146,7 +144,7 @@ object BFTSMaRt {
|
||||
ClusterResponse.Signatures(accepted.map { it.txSignature })
|
||||
} else {
|
||||
log.debug { "Cluster response - error: ${rejected.first().error}" }
|
||||
ClusterResponse.Error(rejected.first().error)
|
||||
ClusterResponse.Error(rejected.map { it.error })
|
||||
}
|
||||
|
||||
val messageContent = aggregateResponse.serialize().bytes
|
||||
@ -242,10 +240,9 @@ object BFTSMaRt {
|
||||
}
|
||||
} else {
|
||||
log.debug { "Conflict detected – the following inputs have already been committed: ${conflicts.keys.joinToString()}" }
|
||||
val conflict = UniquenessProvider.Conflict(conflicts)
|
||||
val conflictData = conflict.serialize()
|
||||
val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
|
||||
throw NotaryException(NotaryError.Conflict(txId, signedConflict))
|
||||
val conflict = conflicts.mapValues { StateConsumptionDetails(it.value.id.sha256()) }
|
||||
val error = NotaryError.Conflict(txId, conflict)
|
||||
throw NotaryInternalException(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,20 +17,19 @@ import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryInternalException
|
||||
import net.corda.core.flows.StateConsumptionDetails
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.UniquenessException
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.config.MySQLConfiguration
|
||||
import java.security.PublicKey
|
||||
import java.sql.BatchUpdateException
|
||||
import java.sql.Connection
|
||||
import java.sql.SQLTransientConnectionException
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
@ -117,7 +116,7 @@ class MySQLUniquenessProvider(
|
||||
} catch (e: BatchUpdateException) {
|
||||
log.info("Unable to commit input states, finding conflicts, txId: $txId", e)
|
||||
conflictCounter.inc()
|
||||
retryTransaction(FindConflicts(states))
|
||||
retryTransaction(FindConflicts(txId, states))
|
||||
} finally {
|
||||
val dt = s.stop().elapsed(TimeUnit.MILLISECONDS)
|
||||
commitTimer.update(dt, TimeUnit.MILLISECONDS)
|
||||
@ -173,26 +172,22 @@ class MySQLUniquenessProvider(
|
||||
}
|
||||
}
|
||||
|
||||
private class FindConflicts(val states: List<StateRef>) : RetryableTransaction {
|
||||
private class FindConflicts(val txId: SecureHash, val states: List<StateRef>) : RetryableTransaction {
|
||||
override fun run(conn: Connection) {
|
||||
val conflicts = mutableMapOf<StateRef, UniquenessProvider.ConsumingTx>()
|
||||
val conflicts = mutableMapOf<StateRef, StateConsumptionDetails>()
|
||||
states.forEach {
|
||||
val st = conn.prepareStatement(findStatement).apply {
|
||||
setBytes(1, it.txhash.bytes)
|
||||
setInt(2, it.index)
|
||||
}
|
||||
val result = st.executeQuery()
|
||||
|
||||
if (result.next()) {
|
||||
val consumingTxId = SecureHash.SHA256(result.getBytes(1))
|
||||
val inputIndex = result.getInt(2)
|
||||
val partyName = CordaX500Name.parse(result.getString(3))
|
||||
val partyKey: PublicKey = result.getBytes(4).deserialize()
|
||||
conflicts[it] = UniquenessProvider.ConsumingTx(consumingTxId, inputIndex, Party(partyName, partyKey))
|
||||
conflicts[it] = StateConsumptionDetails(consumingTxId.sha256())
|
||||
}
|
||||
}
|
||||
conn.commit()
|
||||
if (conflicts.isNotEmpty()) throw UniquenessException(UniquenessProvider.Conflict(conflicts))
|
||||
if (conflicts.isNotEmpty()) throw NotaryInternalException(NotaryError.Conflict(txId, conflicts))
|
||||
}
|
||||
}
|
||||
}
|
@ -13,10 +13,13 @@ package net.corda.node.services.transactions
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryInternalException
|
||||
import net.corda.core.flows.StateConsumptionDetails
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.node.services.UniquenessException
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
@ -78,8 +81,9 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok
|
||||
toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) },
|
||||
fromPersistentEntity = {
|
||||
//TODO null check will become obsolete after making DB/JPA columns not nullable
|
||||
var txId = it.id.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId")
|
||||
var index = it.id.index ?: throw IllegalStateException("DB returned null SecureHash index")
|
||||
val txId = it.id.txId
|
||||
?: throw IllegalStateException("DB returned null SecureHash transactionId")
|
||||
val index = it.id.index ?: throw IllegalStateException("DB returned null SecureHash index")
|
||||
Pair(StateRef(txhash = SecureHash.parse(txId), index = index),
|
||||
UniquenessProvider.ConsumingTx(
|
||||
id = SecureHash.parse(it.consumingTxHash),
|
||||
@ -101,7 +105,6 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok
|
||||
}
|
||||
|
||||
override fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party) {
|
||||
|
||||
val conflict = mutex.locked {
|
||||
val conflictingStates = LinkedHashMap<StateRef, UniquenessProvider.ConsumingTx>()
|
||||
for (inputState in states) {
|
||||
@ -110,7 +113,8 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok
|
||||
}
|
||||
if (conflictingStates.isNotEmpty()) {
|
||||
log.debug("Failure, input states already committed: ${conflictingStates.keys}")
|
||||
UniquenessProvider.Conflict(conflictingStates)
|
||||
val conflict = conflictingStates.mapValues { StateConsumptionDetails(it.value.id.sha256()) }
|
||||
conflict
|
||||
} else {
|
||||
states.forEachIndexed { i, stateRef ->
|
||||
committedStates[stateRef] = UniquenessProvider.ConsumingTx(txId, i, callerIdentity)
|
||||
@ -120,6 +124,6 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok
|
||||
}
|
||||
}
|
||||
|
||||
if (conflict != null) throw UniquenessException(conflict)
|
||||
if (conflict != null) throw NotaryInternalException(NotaryError.Conflict(txId, conflict))
|
||||
}
|
||||
}
|
||||
|
@ -29,8 +29,11 @@ import io.atomix.copycat.server.storage.Storage
|
||||
import io.atomix.copycat.server.storage.StorageLevel
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryInternalException
|
||||
import net.corda.core.flows.StateConsumptionDetails
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.UniquenessException
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
@ -214,7 +217,11 @@ class RaftUniquenessProvider(private val transportConfiguration: NodeSSLConfigur
|
||||
val commitCommand = DistributedImmutableMap.Commands.PutAll(encode(entries))
|
||||
val conflicts = client.submit(commitCommand).get()
|
||||
|
||||
if (conflicts.isNotEmpty()) throw UniquenessException(UniquenessProvider.Conflict(decode(conflicts)))
|
||||
if (conflicts.isNotEmpty()) {
|
||||
val conflictingStates = decode(conflicts).mapValues { StateConsumptionDetails(it.value.id.sha256()) }
|
||||
val error = NotaryError.Conflict(txId, conflictingStates)
|
||||
throw NotaryInternalException(error)
|
||||
}
|
||||
log.debug("All input states of transaction $txId have been committed")
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
|
||||
} catch (e: Exception) {
|
||||
throw when (e) {
|
||||
is TransactionVerificationException,
|
||||
is SignatureException -> NotaryException(NotaryError.TransactionInvalid(e))
|
||||
is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||
else -> e
|
||||
}
|
||||
}
|
||||
@ -77,7 +77,7 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
|
||||
try {
|
||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||
} catch (e: SignatureException) {
|
||||
throw NotaryException(NotaryError.TransactionInvalid(e))
|
||||
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,10 +13,7 @@ package net.corda.node.services.transactions
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.sign
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.generateSignature
|
||||
@ -147,32 +144,45 @@ class NotaryServiceTests {
|
||||
|
||||
@Test
|
||||
fun `should report conflict when inputs are reused across transactions`() {
|
||||
val inputState = issueState(aliceNode.services, alice)
|
||||
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(inputState)
|
||||
.addInputState(state)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
val stx2 = run {
|
||||
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(inputState)
|
||||
.addInputState(issueState(aliceNode.services, alice))
|
||||
.addInputState(firstState)
|
||||
.addInputState(secondState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val firstSpend = NotaryFlow.Client(stx)
|
||||
val secondSpend = NotaryFlow.Client(stx2) // Double spend the inputState in a second transaction.
|
||||
aliceNode.services.startFlow(firstSpend)
|
||||
val future = aliceNode.services.startFlow(secondSpend)
|
||||
|
||||
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, stx2.id)
|
||||
notaryError.conflict.verified()
|
||||
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
|
||||
|
@ -11,8 +11,10 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.NotaryInternalException
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.node.services.UniquenessException
|
||||
import net.corda.node.internal.configureDatabase
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
@ -70,12 +72,11 @@ class PersistentUniquenessProviderTests {
|
||||
val inputs = listOf(inputState)
|
||||
provider.commit(inputs, txID, identity)
|
||||
|
||||
val ex = assertFailsWith<UniquenessException> { provider.commit(inputs, txID, identity) }
|
||||
val ex = assertFailsWith<NotaryInternalException> { provider.commit(inputs, txID, identity) }
|
||||
val error = ex.error as NotaryError.Conflict
|
||||
|
||||
val consumingTx = ex.error.stateHistory[inputState]!!
|
||||
assertEquals(consumingTx.id, txID)
|
||||
assertEquals(consumingTx.inputIndex, inputs.indexOf(inputState))
|
||||
assertEquals(consumingTx.requestingParty, identity)
|
||||
val conflictCause = error.consumedStates[inputState]!!
|
||||
assertEquals(conflictCause.hashOfTransactionId, txID.sha256())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.api.StartedNodeServices
|
||||
import net.corda.node.services.issueInvalidState
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
@ -90,14 +89,13 @@ class ValidatingNotaryServiceTests {
|
||||
aliceNode.services.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val ex = assertFailsWith(NotaryException::class) {
|
||||
// Expecting SignaturesMissingException instead of NotaryException, since the exception should originate from
|
||||
// the client flow.
|
||||
val ex = assertFailsWith<SignedTransaction.SignaturesMissingException> {
|
||||
val future = runClient(stx)
|
||||
future.getOrThrow()
|
||||
}
|
||||
val notaryError = ex.error as NotaryError.TransactionInvalid
|
||||
assertThat(notaryError.cause).isInstanceOf(SignedTransaction.SignaturesMissingException::class.java)
|
||||
|
||||
val missingKeys = (notaryError.cause as SignedTransaction.SignaturesMissingException).missing
|
||||
val missingKeys = ex.missing
|
||||
assertEquals(setOf(expectedMissingKey), missingKeys)
|
||||
}
|
||||
|
||||
|
@ -14,10 +14,14 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import com.nhaarman.mockito_kotlin.argThat
|
||||
import com.nhaarman.mockito_kotlin.doNothing
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.NullKeys
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.internal.NotaryChangeTransactionBuilder
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.node.StatesToRecord
|
||||
import net.corda.core.node.services.StatesNotAvailableException
|
||||
@ -27,7 +31,6 @@ import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.PageSpecification
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.QueryCriteria.*
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
@ -617,7 +620,7 @@ class NodeVaultServiceTest {
|
||||
// Change notary
|
||||
services.identityService.verifyAndRegisterIdentity(DUMMY_NOTARY_IDENTITY)
|
||||
val newNotary = DUMMY_NOTARY
|
||||
val changeNotaryTx = NotaryChangeWireTransaction(listOf(initialCashState.ref), issueStx.notary!!, newNotary)
|
||||
val changeNotaryTx = NotaryChangeTransactionBuilder(listOf(initialCashState.ref), issueStx.notary!!, newNotary).build()
|
||||
val cashStateWithNewNotary = StateAndRef(initialCashState.state.copy(notary = newNotary), StateRef(changeNotaryTx.id, 0))
|
||||
|
||||
database.transaction {
|
||||
|
@ -14,14 +14,11 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.internal.ResolveTransactionsFlow
|
||||
import net.corda.core.internal.validateRequest
|
||||
import net.corda.core.node.AppServiceHub
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionWithSignatures
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
@ -68,7 +65,7 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
||||
} catch (e: Exception) {
|
||||
throw when (e) {
|
||||
is TransactionVerificationException,
|
||||
is SignatureException -> NotaryException(NotaryError.TransactionInvalid(e))
|
||||
is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||
else -> e
|
||||
}
|
||||
}
|
||||
@ -99,7 +96,7 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
||||
try {
|
||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||
} catch (e: SignatureException) {
|
||||
throw NotaryException(NotaryError.TransactionInvalid(e))
|
||||
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import javafx.collections.ObservableList
|
||||
import javafx.geometry.Insets
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.chart.NumberAxis
|
||||
import javafx.scene.chart.XYChart
|
||||
import javafx.scene.control.*
|
||||
import javafx.scene.input.MouseButton
|
||||
import javafx.scene.layout.BorderPane
|
||||
@ -323,12 +324,26 @@ class CashViewer : CordaView("Cash") {
|
||||
linechart(null, xAxis, yAxis) {
|
||||
series("USD") {
|
||||
sumAmount.addListener { _, _, _ ->
|
||||
val lastAmount = data.last().value?.yValue
|
||||
val currAmount = sumAmount.value.toDecimal()
|
||||
val lastTimeStamp = data.last().value?.xValue
|
||||
if (lastTimeStamp == null || System.currentTimeMillis() - lastTimeStamp.toLong() > 1.seconds.toMillis()) {
|
||||
data(System.currentTimeMillis(), sumAmount.value.quantity)
|
||||
runInFxApplicationThread {
|
||||
// Modify data in UI thread.
|
||||
if (data.size > 300) data.remove(0, 1)
|
||||
val currentTimeStamp = System.currentTimeMillis()
|
||||
|
||||
// If amount is not the same - always add a data point.
|
||||
if (lastAmount == null || lastAmount != currAmount) {
|
||||
// If update arrived in very close succession to the previous one - kill the last point received to eliminate un-necessary noise on the graph.
|
||||
if(lastTimeStamp != null && currentTimeStamp - lastTimeStamp.toLong() < 1.seconds.toMillis()) {
|
||||
data.safelyTransition {
|
||||
remove(size - 1, size)
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new data point.
|
||||
data(currentTimeStamp, currAmount)
|
||||
|
||||
// Limit population of data points to make graph painting faster.
|
||||
data.safelyTransition {
|
||||
if (size > 300) remove(0, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -337,5 +352,12 @@ class CashViewer : CordaView("Cash") {
|
||||
animated = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun <X, Y> ObservableList<XYChart.Data<X, Y>>.safelyTransition(block: ObservableList<XYChart.Data<X, Y>>.() -> Unit) {
|
||||
runInFxApplicationThread {
|
||||
// Modify data in UI thread to properly propagate to GUI.
|
||||
this.block()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user