mirror of
https://github.com/corda/corda.git
synced 2024-12-23 14:52:29 +00:00
Fixed AbstractNode to load custom notary services properly (#1720)
* Fixed AbstractNode to load custom notary services properly. Added a custom notary sample. * Prevent multiple custom notaries from being loaded * Throw if more than once custom notary service is loaded
This commit is contained in:
parent
544b761682
commit
7b10e92819
core/src/main/kotlin/net/corda/core/node/services
docs/source
node/src/main/kotlin/net/corda/node
samples
bank-of-corda-demo/src/main/kotlin/net/corda/bank
notary-demo
@ -1,5 +1,6 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import com.google.common.primitives.Booleans
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.*
|
||||
@ -15,12 +16,13 @@ import java.security.PublicKey
|
||||
abstract class NotaryService : SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
const val ID_PREFIX = "corda.notary."
|
||||
fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false): String {
|
||||
require(!raft || !bft)
|
||||
fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false, custom: Boolean = false): String {
|
||||
require(Booleans.countTrue(raft, bft, custom) <= 1) { "At most one of raft, bft or custom may be true" }
|
||||
return StringBuffer(ID_PREFIX).apply {
|
||||
append(if (validating) "validating" else "simple")
|
||||
if (raft) append(".raft")
|
||||
if (bft) append(".bft")
|
||||
if (custom) append(".custom")
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ path to the node's base directory.
|
||||
.. note:: The driver will not automatically create a webserver instance, but the Cordformation will. If this field
|
||||
is present the web server will start.
|
||||
|
||||
:notary: Optional config object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt
|
||||
:notary: Optional configuration object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt
|
||||
cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both.
|
||||
|
||||
:validating: Boolean to determine whether the notary is a validating or non-validating one.
|
||||
@ -108,11 +108,15 @@ path to the node's base directory.
|
||||
members must be active and be able to communicate with the cluster leader for joining. If empty, a new
|
||||
cluster will be bootstrapped.
|
||||
|
||||
:bftSMaRt: If part of a distributed BFT SMaRt cluster specify this config object, with the following settings:
|
||||
:bftSMaRt: If part of a distributed BFT-SMaRt cluster specify this config object, with the following settings:
|
||||
|
||||
:replicaId:
|
||||
:replicaId: The zero-based index of the current replica. All replicas must specify a unique replica id.
|
||||
|
||||
:clusterAddresses:
|
||||
:clusterAddresses: List of all BFT-SMaRt cluster member addresses.
|
||||
|
||||
:custom: If `true`, will load and install a notary service from a CorDapp. See :doc:`tutorial-custom-notary`.
|
||||
|
||||
Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified.
|
||||
|
||||
:networkMapService: If `null`, or missing the node is declaring itself as the NetworkMapService host. Otherwise this is
|
||||
a config object with the details of the network map service:
|
||||
|
@ -35,7 +35,7 @@ To run from the command line in Unix:
|
||||
2. Run ``./samples/trader-demo/build/nodes/runnodes`` to open up four new terminals with the four nodes
|
||||
3. Run ``./gradlew samples:trader-demo:runBank`` to instruct the bank node to issue cash and commercial paper to the buyer and seller nodes respectively.
|
||||
4. Run ``./gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch``
|
||||
|
||||
|
||||
you can see flows running on both sides of transaction. Additionally you should see final trade information displayed
|
||||
to your terminal.
|
||||
|
||||
@ -45,7 +45,7 @@ To run from the command line in Windows:
|
||||
2. Run ``samples\trader-demo\build\nodes\runnodes`` to open up four new terminals with the four nodes
|
||||
3. Run ``gradlew samples:trader-demo:runBank`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node
|
||||
4. Run ``gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch``
|
||||
|
||||
|
||||
you can see flows running on both sides of transaction. Additionally you should see final trade information displayed
|
||||
to your terminal.
|
||||
|
||||
@ -112,8 +112,11 @@ Notary demo
|
||||
This demo shows a party getting transactions notarised by either a single-node or a distributed notary service.
|
||||
All versions of the demo start two counterparty nodes.
|
||||
One of the counterparties will generate transactions that transfer a self-issued asset to the other party and submit them for notarisation.
|
||||
The `Raft <https://raft.github.io/>`_ version of the demo will start three distributed notary nodes.
|
||||
The `BFT SMaRt <https://bft-smart.github.io/library/>`_ version of the demo will start four distributed notary nodes.
|
||||
|
||||
* The `Raft <https://raft.github.io/>`_ version of the demo will start three distributed notary nodes.
|
||||
* The `BFT SMaRt <https://bft-smart.github.io/library/>`_ version of the demo will start four distributed notary nodes.
|
||||
* The Single version of the demo will start a single-node validating notary service.
|
||||
* The Custom version of the demo will load and start a custom single-node notary service that is defined the demo CorDapp.
|
||||
|
||||
The output will display a list of notarised transaction IDs and corresponding signer public keys. In the Raft distributed notary,
|
||||
every node in the cluster can service client requests, and one signature is sufficient to satisfy the notary composite key requirement.
|
||||
@ -122,9 +125,9 @@ You will notice that successive transactions get signed by different members of
|
||||
|
||||
To run the Raft version of the demo from the command line in Unix:
|
||||
|
||||
1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories
|
||||
with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and
|
||||
Single notaries).
|
||||
1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create node directories for all versions of the demo,
|
||||
with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT``, ``nodesSingle``, and ``nodesCustom`` for
|
||||
BFT, Single and Custom notaries respectively).
|
||||
2. Run ``./samples/notary-demo/build/nodes/nodesRaft/runnodes``, which will start the nodes in separate terminal windows/tabs.
|
||||
Wait until a "Node started up and registered in ..." message appears on each of the terminals
|
||||
3. Run ``./gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests
|
||||
@ -133,8 +136,8 @@ To run the Raft version of the demo from the command line in Unix:
|
||||
To run from the command line in Windows:
|
||||
|
||||
1. Run ``gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories
|
||||
with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and
|
||||
Single notaries).
|
||||
with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT``, ``nodesSingle``, and ``nodesCustom`` for
|
||||
BFT, Single and Custom notaries respectively).
|
||||
2. Run ``samples\notary-demo\build\nodes\nodesRaft\runnodes``, which will start the nodes in separate terminal windows/tabs.
|
||||
Wait until a "Node started up and registered in ..." message appears on each of the terminals
|
||||
3. Run ``gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests
|
||||
@ -142,6 +145,7 @@ To run from the command line in Windows:
|
||||
|
||||
To run the BFT SMaRt notary demo, use ``nodesBFT`` instead of ``nodesRaft`` in the path (you will see messages from notary nodes
|
||||
trying to communicate each other sometime with connection errors, that's normal). For a single notary node, use ``nodesSingle``.
|
||||
For the custom notary service use ``nodesCustom`.
|
||||
|
||||
Distributed notary nodes store consumed states in a replicated commit log, which is backed by a H2 database on each node.
|
||||
You can ascertain that the commit log is synchronised across the cluster by accessing and comparing each of the nodes' backing stores
|
||||
|
@ -1,17 +1,17 @@
|
||||
.. highlight:: kotlin
|
||||
|
||||
Writing a custom notary service
|
||||
===============================
|
||||
Writing a custom notary service (experimental)
|
||||
==============================================
|
||||
|
||||
.. warning:: Customising a notary service is an advanced feature and not recommended for most use-cases. Currently,
|
||||
.. 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.
|
||||
|
||||
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 only requirement
|
||||
is that the class provide a constructor with a single parameter of type ``AppServiceHub``.
|
||||
with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The custom notary
|
||||
service class should provide a constructor with two parameters of types ``AppServiceHub`` and ``PublicKey``.
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt
|
||||
.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt
|
||||
:language: kotlin
|
||||
:start-after: START 1
|
||||
:end-before: END 1
|
||||
@ -20,7 +20,16 @@ The next step is to write a notary service flow. You are free to copy and modify
|
||||
as ``ValidatingNotaryFlow``, ``NonValidatingNotaryFlow``, or implement your own from scratch (following the
|
||||
``NotaryFlow.Service`` template). Below is an example of a custom flow for a *validating* notary service:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt
|
||||
.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt
|
||||
:language: kotlin
|
||||
:start-after: START 2
|
||||
:end-before: END 2
|
||||
|
||||
To enable the service, add the following to the node configuration:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
notary : {
|
||||
validating : true # Set to false if your service is non-validating
|
||||
custom : true
|
||||
}
|
@ -16,6 +16,7 @@ Tutorials
|
||||
flow-testing
|
||||
running-a-notary
|
||||
oracles
|
||||
tutorial-custom-notary
|
||||
tutorial-tear-offs
|
||||
tutorial-attachments
|
||||
event-scheduling
|
@ -19,14 +19,11 @@ import net.corda.core.internal.concurrent.flatMap
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.node.*
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.AppServiceHub
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.StateLoader
|
||||
import net.corda.core.node.services.*
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
@ -257,7 +254,8 @@ abstract class AbstractNode(config: NodeConfiguration,
|
||||
private class ServiceInstantiationException(cause: Throwable?) : CordaException("Service Instantiation Error", cause)
|
||||
|
||||
private fun installCordaServices() {
|
||||
cordappProvider.cordapps.flatMap { it.services }.forEach {
|
||||
val loadedServices = cordappProvider.cordapps.flatMap { it.services }
|
||||
filterServicesToInstall(loadedServices).forEach {
|
||||
try {
|
||||
installCordaService(it)
|
||||
} catch (e: NoSuchMethodException) {
|
||||
@ -271,6 +269,25 @@ abstract class AbstractNode(config: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterServicesToInstall(loadedServices: List<Class<out SerializeAsToken>>): List<Class<out SerializeAsToken>> {
|
||||
val customNotaryServiceList = loadedServices.filter { isNotaryService(it) }
|
||||
if (customNotaryServiceList.isNotEmpty()) {
|
||||
if (configuration.notary?.custom == true) {
|
||||
require(customNotaryServiceList.size == 1) {
|
||||
"Attempting to install more than one notary service: ${customNotaryServiceList.joinToString()}"
|
||||
}
|
||||
}
|
||||
else return loadedServices - customNotaryServiceList
|
||||
}
|
||||
return loadedServices
|
||||
}
|
||||
|
||||
/**
|
||||
* If the [serviceClass] is a notary service, it will only be enable if the "custom" flag is set in
|
||||
* the notary configuration.
|
||||
*/
|
||||
private fun isNotaryService(serviceClass: Class<*>) = NotaryService::class.java.isAssignableFrom(serviceClass)
|
||||
|
||||
/**
|
||||
* This customizes the ServiceHub for each CordaService that is initiating flows
|
||||
*/
|
||||
@ -321,14 +338,15 @@ abstract class AbstractNode(config: NodeConfiguration,
|
||||
fun <T : SerializeAsToken> installCordaService(serviceClass: Class<T>): T {
|
||||
serviceClass.requireAnnotation<CordaService>()
|
||||
val service = try {
|
||||
if (NotaryService::class.java.isAssignableFrom(serviceClass)) {
|
||||
val serviceContext = AppServiceHubImpl<T>(services)
|
||||
if (isNotaryService(serviceClass)) {
|
||||
check(myNotaryIdentity != null) { "Trying to install a notary service but no notary identity specified" }
|
||||
val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true }
|
||||
constructor.newInstance(services, myNotaryIdentity!!.owningKey)
|
||||
val constructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true }
|
||||
serviceContext.serviceInstance = constructor.newInstance(serviceContext, myNotaryIdentity!!.owningKey)
|
||||
serviceContext.serviceInstance
|
||||
} else {
|
||||
try {
|
||||
val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true }
|
||||
val serviceContext = AppServiceHubImpl<T>(services)
|
||||
serviceContext.serviceInstance = extendedServiceConstructor.newInstance(serviceContext)
|
||||
serviceContext.serviceInstance
|
||||
} catch (ex: NoSuchMethodException) {
|
||||
@ -688,7 +706,9 @@ abstract class AbstractNode(config: NodeConfiguration,
|
||||
// Node's main identity
|
||||
Pair("identity", myLegalName)
|
||||
} else {
|
||||
val notaryId = notaryConfig.run { NotaryService.constructId(validating, raft != null, bftSMaRt != null) }
|
||||
val notaryId = notaryConfig.run {
|
||||
NotaryService.constructId(validating, raft != null, bftSMaRt != null, custom)
|
||||
}
|
||||
if (notaryConfig.bftSMaRt == null && notaryConfig.raft == null) {
|
||||
// Node's notary identity
|
||||
Pair(notaryId, myLegalName.copy(commonName = notaryId))
|
||||
|
@ -36,9 +36,15 @@ interface NodeConfiguration : NodeSSLConfiguration {
|
||||
val additionalNodeInfoPollingFrequencyMsec: Long
|
||||
}
|
||||
|
||||
data class NotaryConfig(val validating: Boolean, val raft: RaftConfig? = null, val bftSMaRt: BFTSMaRtConfiguration? = null) {
|
||||
data class NotaryConfig(val validating: Boolean,
|
||||
val raft: RaftConfig? = null,
|
||||
val bftSMaRt: BFTSMaRtConfiguration? = null,
|
||||
val custom: Boolean = false
|
||||
) {
|
||||
init {
|
||||
require(raft == null || bftSMaRt == null) { "raft and bftSMaRt configs cannot be specified together" }
|
||||
require(raft == null || bftSMaRt == null || !custom) {
|
||||
"raft, bftSMaRt, and custom configs cannot be specified together"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,9 +52,10 @@ data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses:
|
||||
|
||||
/** @param exposeRaces for testing only, so its default is not in reference.conf but here. */
|
||||
data class BFTSMaRtConfiguration constructor(val replicaId: Int,
|
||||
val clusterAddresses: List<NetworkHostAndPort>,
|
||||
val debug: Boolean = false,
|
||||
val exposeRaces: Boolean = false) {
|
||||
val clusterAddresses: List<NetworkHostAndPort>,
|
||||
val debug: Boolean = false,
|
||||
val exposeRaces: Boolean = false
|
||||
) {
|
||||
init {
|
||||
require(replicaId >= 0) { "replicaId cannot be negative" }
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ private class BankOfCordaDriver {
|
||||
val bigCorpUser = User(BIGCORP_USERNAME, "test",
|
||||
permissions = setOf(
|
||||
startFlowPermission<CashPaymentFlow>()))
|
||||
startNotaryNode(DUMMY_NOTARY.name, validating = false)
|
||||
startNotaryNode(DUMMY_NOTARY.name, validating = true)
|
||||
val bankOfCorda = startNode(
|
||||
providedName = BOC.name,
|
||||
rpcUsers = listOf(bankUser))
|
||||
|
@ -46,13 +46,18 @@ publishing {
|
||||
}
|
||||
}
|
||||
|
||||
task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT'])
|
||||
task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT', 'deployNodesCustom'])
|
||||
|
||||
task deployNodesSingle(type: Cordform, dependsOn: 'jar') {
|
||||
directory "./build/nodes/nodesSingle"
|
||||
definitionClass = 'net.corda.notarydemo.SingleNotaryCordform'
|
||||
}
|
||||
|
||||
task deployNodesCustom(type: Cordform, dependsOn: 'jar') {
|
||||
directory "./build/nodes/nodesCustom"
|
||||
definitionClass = 'net.corda.notarydemo.CustomNotaryCordform'
|
||||
}
|
||||
|
||||
task deployNodesRaft(type: Cordform, dependsOn: 'jar') {
|
||||
directory "./build/nodes/nodesRaft"
|
||||
definitionClass = 'net.corda.notarydemo.RaftNotaryCordform'
|
||||
|
@ -0,0 +1,36 @@
|
||||
package net.corda.notarydemo
|
||||
|
||||
import net.corda.cordform.CordformContext
|
||||
import net.corda.cordform.CordformDefinition
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.node.services.config.NotaryConfig
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.internal.demorun.*
|
||||
|
||||
fun main(args: Array<String>) = CustomNotaryCordform.runNodes()
|
||||
|
||||
object CustomNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") {
|
||||
init {
|
||||
node {
|
||||
name(ALICE.name)
|
||||
p2pPort(10002)
|
||||
rpcPort(10003)
|
||||
rpcUsers(notaryDemoUser)
|
||||
}
|
||||
node {
|
||||
name(BOB.name)
|
||||
p2pPort(10005)
|
||||
rpcPort(10006)
|
||||
}
|
||||
node {
|
||||
name(DUMMY_NOTARY.name)
|
||||
p2pPort(10009)
|
||||
rpcPort(10010)
|
||||
notary(NotaryConfig(validating = true, custom = true))
|
||||
}
|
||||
}
|
||||
|
||||
override fun setup(context: CordformContext) {}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package net.corda.docs
|
||||
package net.corda.notarydemo
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
@ -8,11 +8,17 @@ import net.corda.core.node.AppServiceHub
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.node.services.TimeWindowChecker
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionWithSignatures
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
* A custom notary service should provide a constructor that accepts two parameters of types [AppServiceHub] and [PublicKey].
|
||||
*
|
||||
* Note that at present only a single-node notary service can be customised.
|
||||
*/
|
||||
// START 1
|
||||
@CordaService
|
||||
class MyCustomValidatingNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() {
|
||||
@ -26,6 +32,7 @@ class MyCustomValidatingNotaryService(override val services: AppServiceHub, over
|
||||
}
|
||||
// END 1
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
// START 2
|
||||
class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryFlow.Service(otherSide, service) {
|
||||
/**
|
||||
@ -38,11 +45,15 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
||||
val notary = stx.notary
|
||||
checkNotary(notary)
|
||||
val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction())
|
||||
null
|
||||
else
|
||||
stx.tx.timeWindow
|
||||
val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub)
|
||||
var timeWindow: TimeWindow? = null
|
||||
val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(serviceHub)
|
||||
} else {
|
||||
val wtx = stx.tx
|
||||
customVerify(wtx.toLedgerTransaction(serviceHub))
|
||||
timeWindow = wtx.timeWindow
|
||||
stx
|
||||
}
|
||||
checkSignatures(transactionWithSignatures)
|
||||
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
|
||||
} catch (e: Exception) {
|
||||
@ -54,6 +65,10 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
||||
}
|
||||
}
|
||||
|
||||
private fun customVerify(transaction: LedgerTransaction) {
|
||||
// Add custom verification logic
|
||||
}
|
||||
|
||||
private fun checkSignatures(tx: TransactionWithSignatures) {
|
||||
try {
|
||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
Loading…
Reference in New Issue
Block a user