mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Collect Signatures now works with encryption
This commit is contained in:
parent
16a4c92677
commit
9e46423465
@ -222,7 +222,9 @@ private class WireTransactionSerializer : JsonSerializer<WireTransaction>() {
|
||||
value.attachments,
|
||||
value.references,
|
||||
value.privacySalt,
|
||||
value.networkParametersHash
|
||||
value.networkParametersHash,
|
||||
value.inputsStates,
|
||||
value.referenceStates
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -238,7 +240,9 @@ private class WireTransactionDeserializer : JsonDeserializer<WireTransaction>()
|
||||
wrapper.notary,
|
||||
wrapper.timeWindow,
|
||||
wrapper.references,
|
||||
wrapper.networkParametersHash
|
||||
wrapper.networkParametersHash,
|
||||
wrapper.inputStates,
|
||||
wrapper.referenceStates
|
||||
)
|
||||
return WireTransaction(componentGroups, wrapper.privacySalt, wrapper.digestService ?: DigestService.sha2_256)
|
||||
}
|
||||
@ -254,7 +258,9 @@ private class WireTransactionJson(@get:JsonInclude(Include.NON_NULL) val digestS
|
||||
val attachments: List<SecureHash>,
|
||||
val references: List<StateRef>,
|
||||
val privacySalt: PrivacySalt,
|
||||
val networkParametersHash: SecureHash?)
|
||||
val networkParametersHash: SecureHash?,
|
||||
val inputStates: List<StateAndRef<ContractState>>,
|
||||
val referenceStates: List<StateAndRef<ContractState>>)
|
||||
|
||||
private interface TransactionStateMixin {
|
||||
@get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
|
@ -13,5 +13,7 @@ enum class ComponentGroupEnum {
|
||||
TIMEWINDOW_GROUP, // ordinal = 5.
|
||||
SIGNERS_GROUP, // ordinal = 6.
|
||||
REFERENCES_GROUP, // ordinal = 7.
|
||||
PARAMETERS_GROUP // ordinal = 8.
|
||||
PARAMETERS_GROUP, // ordinal = 8.
|
||||
INPUT_STATES_GROUP, // ordinal = 9.
|
||||
REFERENCE_STATES_GROUP, // ordinal = 10.
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.sun.org.apache.xpath.internal.operations.Bool
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.toStringShort
|
||||
@ -8,7 +9,9 @@ import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.groupPublicKeysByWellKnownParty
|
||||
import net.corda.core.internal.dependencies
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.RawDependency
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
@ -203,7 +206,7 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session
|
||||
@Suspendable
|
||||
override fun call(): List<TransactionSignature> {
|
||||
// SendTransactionFlow allows counterparty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(session, partiallySignedTx))
|
||||
subFlow(SendTransactionFlow(session, partiallySignedTx, encrypted = true))
|
||||
// Send the key we expect the counterparty to sign with - this is important where they may have several
|
||||
// keys to sign with, as it makes it faster for them to identify the key to sign with, and more straight forward
|
||||
// for us to check we have the expected signature returned.
|
||||
@ -259,7 +262,7 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session
|
||||
* @param otherSideSession The session which is providing you a transaction to sign.
|
||||
*/
|
||||
abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSession: FlowSession,
|
||||
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker(), val encrypted : Boolean = false) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object RECEIVING : ProgressTracker.Step("Receiving transaction proposal for signing.")
|
||||
@ -274,10 +277,10 @@ abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSessio
|
||||
override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = RECEIVING
|
||||
// Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures.
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false, encrypted = true))
|
||||
// Receive the signing key that the party requesting the signature expects us to sign with. Having this provided
|
||||
// means we only have to check we own that one key, rather than matching all keys in the transaction against all
|
||||
// keys we own.
|
||||
// keys we own.t
|
||||
val signingKeys = otherSideSession.receive<List<PublicKey>>().unwrap { keys ->
|
||||
// TODO: We should have a faster way of verifying we own a single key
|
||||
serviceHub.keyManagementService.filterMyKeys(keys)
|
||||
@ -287,7 +290,53 @@ abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSessio
|
||||
checkMySignaturesRequired(stx, signingKeys)
|
||||
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
|
||||
checkSignatures(stx)
|
||||
stx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
if (encrypted) {
|
||||
val encryptionService = serviceHub.encryptedTransactionService
|
||||
val validatedTxSvc = serviceHub.validatedTransactions
|
||||
|
||||
val encryptedTxs = stx.dependencies.mapNotNull {
|
||||
validatedTxId ->
|
||||
validatedTxSvc.getEncryptedTransaction(validatedTxId)?.let { etx ->
|
||||
etx.id to etx
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
val signedTxs = stx.dependencies.mapNotNull {
|
||||
validatedTxId ->
|
||||
validatedTxSvc.getTransaction(validatedTxId)?.let { stx ->
|
||||
stx.id to stx
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
val networkParameters = stx.dependencies.mapNotNull { depTxId ->
|
||||
val npHash = when {
|
||||
encryptedTxs[depTxId] != null -> serviceHub.encryptedTransactionService.getNetworkParameterHash(encryptedTxs[depTxId]!!)
|
||||
?: serviceHub.networkParametersService.defaultHash
|
||||
signedTxs[depTxId] != null -> signedTxs[depTxId]!!.networkParametersHash
|
||||
?: serviceHub.networkParametersService.defaultHash
|
||||
else -> null
|
||||
}
|
||||
|
||||
npHash?.let { depTxId to npHash }
|
||||
}.associate {
|
||||
netParams ->
|
||||
netParams.first to serviceHub.networkParametersService.lookup(netParams.second)
|
||||
}
|
||||
|
||||
val rawDependencies = stx.dependencies.associate {
|
||||
txId ->
|
||||
txId to RawDependency(
|
||||
encryptedTxs[txId],
|
||||
signedTxs[txId],
|
||||
networkParameters[txId]
|
||||
)
|
||||
}
|
||||
|
||||
encryptionService.verifyTransaction(stx, serviceHub, false, rawDependencies)
|
||||
} else {
|
||||
|
||||
stx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
}
|
||||
// Perform some custom verification over the transaction.
|
||||
try {
|
||||
checkTransaction(stx)
|
||||
|
@ -153,7 +153,9 @@ fun createComponentGroups(inputs: List<StateRef>,
|
||||
notary: Party?,
|
||||
timeWindow: TimeWindow?,
|
||||
references: List<StateRef>,
|
||||
networkParametersHash: SecureHash?): List<ComponentGroup> {
|
||||
networkParametersHash: SecureHash?,
|
||||
inputStates: List<StateAndRef<ContractState>> = emptyList(),
|
||||
referenceStates: List<StateAndRef<ContractState>> = emptyList()): List<ComponentGroup> {
|
||||
val serializationFactory = SerializationFactory.defaultFactory
|
||||
val serializationContext = serializationFactory.defaultContext
|
||||
val serialize = { value: Any, _: Int -> value.serialize(serializationFactory, serializationContext) }
|
||||
@ -170,6 +172,8 @@ fun createComponentGroups(inputs: List<StateRef>,
|
||||
// a FilteredTransaction can now verify it sees all the commands it should sign.
|
||||
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.SIGNERS_GROUP.ordinal, commands.map { it.signers }.lazyMapped(serialize)))
|
||||
if (networkParametersHash != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.PARAMETERS_GROUP.ordinal, listOf(networkParametersHash.serialize())))
|
||||
if (inputStates.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.INPUT_STATES_GROUP.ordinal, inputStates.lazyMapped(serialize)))
|
||||
if (referenceStates.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.REFERENCE_STATES_GROUP.ordinal, referenceStates.lazyMapped(serialize)))
|
||||
return componentGroupMap
|
||||
}
|
||||
|
||||
|
@ -133,6 +133,9 @@ private class Validator(private val ltx: LedgerTransaction, private val transact
|
||||
validateStatesAgainstContract()
|
||||
|
||||
// 5. Final step will be to run the contract code.
|
||||
|
||||
// Encryption PoC
|
||||
|
||||
}
|
||||
|
||||
private fun checkTransactionWithTimeWindowIsNotarised() {
|
||||
@ -510,6 +513,10 @@ class TransactionVerifier(private val transactionClassLoader: ClassLoader) : Fun
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkInputs() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// BOB
|
||||
|
@ -89,6 +89,18 @@ class EncryptedTransactionService() : SingletonSerializeAsToken() {
|
||||
|
||||
val dependencies = extractDependencies(signedTransaction.inputs + signedTransaction.references, rawDependencies)
|
||||
|
||||
(signedTransaction.tx.inputsStates + signedTransaction.tx.referenceStates).forEach {
|
||||
val dependency = dependencies[it.ref.txhash] ?: throw IllegalArgumentException("Dependency transaction not found")
|
||||
val dependencyState = dependency.inputsAndRefs[it.ref] ?: throw IllegalArgumentException("Dependency state not found")
|
||||
|
||||
val dependentState = dependencyState.data
|
||||
val suppliedState = it.state.data
|
||||
require(dependencyState.data == it.state.data) {
|
||||
|
||||
"Supplied input/ref on transaction did not match it's stateRef"
|
||||
}
|
||||
}
|
||||
|
||||
// will throw if cannot verify
|
||||
signedTransaction.toLedgerTransaction(serviceHub, checkSufficientSignatures, dependencies).verify()
|
||||
}
|
||||
|
@ -24,6 +24,10 @@ abstract class CoreTransaction : BaseTransaction() {
|
||||
* was created on older version of Corda (before 4), resolution will default to initial parameters.
|
||||
*/
|
||||
abstract val networkParametersHash: SecureHash?
|
||||
|
||||
// Encryption PoC - optionally bundle the states
|
||||
open val inputsStates: List<StateAndRef<ContractState>> = emptyList()
|
||||
open val referenceStates: List<StateAndRef<ContractState>> = emptyList()
|
||||
}
|
||||
|
||||
/** A transaction with fully resolved components, such as input states. */
|
||||
|
@ -90,6 +90,7 @@ open class TransactionBuilder(
|
||||
|
||||
private val inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>()
|
||||
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
||||
private val referencesWithTransactionStateAndRef = arrayListOf<StateAndRef<ContractState>>()
|
||||
private val excludedAttachments = arrayListOf<AttachmentId>()
|
||||
|
||||
/**
|
||||
@ -109,6 +110,7 @@ open class TransactionBuilder(
|
||||
)
|
||||
t.inputsWithTransactionState.addAll(this.inputsWithTransactionState)
|
||||
t.referencesWithTransactionState.addAll(this.referencesWithTransactionState)
|
||||
t.referencesWithTransactionStateAndRef.addAll(this.referencesWithTransactionStateAndRef)
|
||||
return t
|
||||
}
|
||||
|
||||
@ -215,7 +217,9 @@ open class TransactionBuilder(
|
||||
notary,
|
||||
window,
|
||||
referenceStates,
|
||||
services.networkParametersService.currentHash),
|
||||
services.networkParametersService.currentHash,
|
||||
inputsWithTransactionState,
|
||||
referencesWithTransactionStateAndRef),
|
||||
privacySalt,
|
||||
services.digestService
|
||||
)
|
||||
@ -758,6 +762,7 @@ open class TransactionBuilder(
|
||||
open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>) = apply {
|
||||
val stateAndRef = referencedStateAndRef.stateAndRef
|
||||
referencesWithTransactionState.add(stateAndRef.state)
|
||||
referencesWithTransactionStateAndRef.add(stateAndRef)
|
||||
|
||||
// It is likely the case that users of reference states do not have permission to change the notary assigned
|
||||
// to a reference state. Even if users _did_ have this permission the result would likely be a bunch of
|
||||
|
@ -18,6 +18,7 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
@ -89,6 +90,22 @@ constructor(componentGroups: List<ComponentGroup>, val privacySalt: PrivacySalt,
|
||||
/** The transaction id is represented by the root hash of Merkle tree over the transaction components. */
|
||||
override val id: SecureHash get() = merkleTree.hash
|
||||
|
||||
// Encryption PoC
|
||||
val serializationFactory = SerializationFactory.defaultFactory
|
||||
val serializationContext = serializationFactory.defaultContext
|
||||
|
||||
override val inputsStates: List<StateAndRef<ContractState>> = componentGroups
|
||||
.singleOrNull{ it.groupIndex == ComponentGroupEnum.INPUT_STATES_GROUP.ordinal }
|
||||
?.let { group ->
|
||||
group.components.map { (it as SerializedBytes<StateAndRef<ContractState>>).deserialize(serializationFactory, serializationContext) }
|
||||
} ?: emptyList()
|
||||
|
||||
override val referenceStates : List<StateAndRef<ContractState>> = componentGroups
|
||||
.singleOrNull{ it.groupIndex == ComponentGroupEnum.REFERENCE_STATES_GROUP.ordinal }
|
||||
?.let { group ->
|
||||
group.components.map {(it as SerializedBytes<StateAndRef<ContractState>>).deserialize(serializationFactory, serializationContext) }
|
||||
} ?: emptyList()
|
||||
|
||||
/** Public keys that need to be fulfilled by signatures in order for the transaction to be valid. */
|
||||
val requiredSigningKeys: Set<PublicKey>
|
||||
get() {
|
||||
|
@ -128,7 +128,7 @@ abstract class OnLedgerAsset<T : Any, out C : CommandData, S : FungibleAsset<T>>
|
||||
// Select a subset of the available states we were given that sums up to >= totalSendAmount.
|
||||
val (gathered, gatheredAmount) = gatherCoins(acceptableStates, totalSendAmount)
|
||||
check(gatheredAmount >= totalSendAmount)
|
||||
val keysUsed = gathered.map { it.state.data.owner.owningKey }
|
||||
val keysUsed = gathered.map { it.state.data.owner.owningKey } //+ payments.map { it.party.owningKey }
|
||||
|
||||
// Now calculate the output states. This is complicated by the fact that a single payment may require
|
||||
// multiple output states, due to the need to keep states separated by issuer. We start by figuring out
|
||||
|
@ -0,0 +1,304 @@
|
||||
package net.corda.node.services.encryptedtx
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.BelongsToContract
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.flows.CollectSignaturesFlow
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.ReceiveFinalityFlow
|
||||
import net.corda.core.flows.SignTransactionFlow
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.toHexString
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.persistence.DBCheckpointStorage
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.BOC_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNetworkParameters
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import net.corda.testing.node.internal.enclosedCordapp
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.lang.IllegalArgumentException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class EncryptedBackchainTests {
|
||||
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var bankOfCordaNode: StartedMockNode
|
||||
private lateinit var bankOfCorda: Party
|
||||
private lateinit var aliceNode: StartedMockNode
|
||||
private lateinit var alice: Party
|
||||
private lateinit var bobNode: StartedMockNode
|
||||
private lateinit var bob: Party
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
mockNet = MockNetwork(MockNetworkParameters(servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin(), cordappsForAllNodes = listOf(enclosedCordapp())))
|
||||
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
|
||||
bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME)
|
||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
alice = aliceNode.info.singleIdentity()
|
||||
bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
bob = bobNode.info.singleIdentity()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue and move`() {
|
||||
|
||||
val issuanceTx = bankOfCordaNode.execFlow(IssueFlow(300))
|
||||
|
||||
val aliceTx = bankOfCordaNode.execFlow(MoveFlow(bankOfCordaNode.getAllTokens(), 200, alice))
|
||||
|
||||
val bobTx = aliceNode.execFlow(MoveFlow(aliceNode.getAllTokens(), 100, bob))
|
||||
|
||||
printTxs( listOf(
|
||||
"Bank issue to self: " to issuanceTx,
|
||||
"Bank pays Alice: " to aliceTx,
|
||||
"Alice pays Bob: " to bobTx)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue and move with remote signer`() {
|
||||
|
||||
val issuanceTx = bankOfCordaNode.execFlow(IssueFlow(300))
|
||||
|
||||
val aliceTx = bankOfCordaNode.execFlow(MoveFlow(bankOfCordaNode.getAllTokens(), 200, alice, true))
|
||||
|
||||
val bobTx = aliceNode.execFlow(MoveFlow(aliceNode.getAllTokens(), 100, bob, true))
|
||||
|
||||
printTxs( listOf(
|
||||
"Bank issue to self: " to issuanceTx,
|
||||
"Bank pays Alice: " to aliceTx,
|
||||
"Alice pays Bob: " to bobTx)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bank of Corda cannot pay bob`() {
|
||||
|
||||
bankOfCordaNode.execFlow(IssueFlow(300))
|
||||
|
||||
val exception = assertFailsWith<FlowException>{
|
||||
bankOfCordaNode.execFlow(MoveFlow(bankOfCordaNode.getAllTokens(), 200, bob, true))
|
||||
}
|
||||
|
||||
assertEquals("java.lang.IllegalArgumentException: Bank of Corda cannot move money to Bob", exception.message)
|
||||
}
|
||||
|
||||
private fun printTxs(txHashes : List<Pair<String,SignedTransaction>>) {
|
||||
listOf(bankOfCordaNode, aliceNode, bobNode).forEach { node ->
|
||||
println("------------------------")
|
||||
println("${node.info.singleIdentity()}")
|
||||
println("------------------------")
|
||||
txHashes.forEach { labelToStx ->
|
||||
val label = labelToStx.first
|
||||
val stx = labelToStx.second
|
||||
println("$label (${stx.id})")
|
||||
println("> FOUND UNENCRYPTED: ${node.services.validatedTransactions.getTransaction(stx.id)}")
|
||||
println("> FOUND ENCRYPTED: ${node.services.validatedTransactions.getVerifiedEncryptedTransaction(stx.id)?.let {
|
||||
"${shortStringDesc(it.bytes.toHexString())} signature ${it.verifierSignature.toHexString()}"
|
||||
}}")
|
||||
|
||||
println()
|
||||
}
|
||||
println()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> StartedMockNode.execFlow(flow : FlowLogic<T>) : T {
|
||||
val future = startFlow(flow)
|
||||
mockNet.runNetwork()
|
||||
return future.getOrThrow()
|
||||
}
|
||||
|
||||
private fun StartedMockNode.getAllTokens() : List<StateAndRef<BasicToken>> {
|
||||
|
||||
val allStates = services.vaultService.queryBy(BasicToken::class.java).states
|
||||
|
||||
println(this.info.singleIdentity())
|
||||
allStates.forEach {
|
||||
println(it.state.data)
|
||||
}
|
||||
|
||||
return allStates.filter {
|
||||
it.state.data.holder.owningKey == this.info.singleIdentity().owningKey
|
||||
}
|
||||
}
|
||||
|
||||
private fun shortStringDesc(longString : String) : String {
|
||||
return "EncryptedTransaction(${longString.take(15)}...${longString.takeLast(15)})"
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class SignaturesRequired {
|
||||
ALL,
|
||||
SENDER_ONLY
|
||||
}
|
||||
|
||||
class BasicTokenContract: Contract {
|
||||
|
||||
companion object {
|
||||
val contractId = this::class.java.enclosingClass.canonicalName
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val command = tx.commandsOfType(BasicTokenCommand::class.java).single()
|
||||
|
||||
when (command.value) {
|
||||
is Issue -> {
|
||||
val inputs = tx.inputsOfType<BasicToken>()
|
||||
val outputs = tx.outputsOfType<BasicToken>()
|
||||
|
||||
require(inputs.isEmpty()) { "No input states allowed" }
|
||||
require(outputs.isNotEmpty()) { "At least one BasicToken input state is required" }
|
||||
require(outputs.all { it.amount > 0 }) { "Outputs must have amounts greater than zero" }
|
||||
|
||||
}
|
||||
is Move -> {
|
||||
val inputs = tx.inputsOfType<BasicToken>()
|
||||
val outputs = tx.outputsOfType<BasicToken>()
|
||||
|
||||
require(inputs.isNotEmpty() && outputs.isNotEmpty()) { "Input and output states are required" }
|
||||
require(inputs.sumBy { it.amount } == outputs.sumBy { it.amount }) { "Inputs and outputs must have the same value"}
|
||||
require(command.signers.containsAll(inputs.map { it.holder.owningKey })) { "All holders must sign the tx" }
|
||||
|
||||
require(inputs.all { it.amount > 0 }) { "Inputs must have amounts greater than zero" }
|
||||
require(outputs.all { it.amount > 0 }) { "Outputs must have amounts greater than zero" }
|
||||
// no restriction on mixing issuers
|
||||
}
|
||||
}
|
||||
}
|
||||
open class BasicTokenCommand : CommandData
|
||||
class Issue : BasicTokenCommand()
|
||||
class Move : BasicTokenCommand()
|
||||
}
|
||||
|
||||
@BelongsToContract(BasicTokenContract::class)
|
||||
class BasicToken(
|
||||
val amount: Int,
|
||||
val holder: AbstractParty,
|
||||
override val participants : List<AbstractParty> = listOf(holder)) : ContractState {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as BasicToken
|
||||
if (amount != other.amount) return false
|
||||
if (holder != other.holder) return false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class IssueFlow(val amount: Int): FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call() : SignedTransaction {
|
||||
val ourKey = ourIdentity.owningKey
|
||||
|
||||
val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first())
|
||||
.addCommand(BasicTokenContract.Issue(), ourKey)
|
||||
.addOutputState(BasicToken(amount, ourIdentity))
|
||||
val stx = serviceHub.signInitialTransaction(tx, ourKey)
|
||||
|
||||
return subFlow(FinalityFlow(stx, emptyList()))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class MoveFlow(val inputs : List<StateAndRef<BasicToken>>,
|
||||
val amount: Int,
|
||||
val moveTo: AbstractParty,
|
||||
val allMustSign : Boolean = false): FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call() : SignedTransaction{
|
||||
val ourKey = ourIdentity.owningKey
|
||||
|
||||
val allMustSignStatus = if(allMustSign) SignaturesRequired.ALL else SignaturesRequired.SENDER_ONLY
|
||||
val signingKeys = if(allMustSign) listOf(ourKey, moveTo.owningKey) else listOf(ourKey)
|
||||
|
||||
val changeAmount = inputs.sumBy { it.state.data.amount } - amount
|
||||
|
||||
val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first())
|
||||
.addCommand(BasicTokenContract.Move(), signingKeys)
|
||||
.addOutputState(BasicToken(amount, moveTo))
|
||||
|
||||
if (changeAmount > 0) {
|
||||
tx.addOutputState(BasicToken(changeAmount, ourIdentity))
|
||||
}
|
||||
|
||||
inputs.forEach {
|
||||
tx.addInputState(it)
|
||||
}
|
||||
|
||||
tx.verify(serviceHub)
|
||||
|
||||
var stx = serviceHub.signInitialTransaction(tx, ourKey)
|
||||
|
||||
val wtx = stx.tx
|
||||
|
||||
val sessions = listOfNotNull(
|
||||
serviceHub.identityService.wellKnownPartyFromAnonymous(moveTo)
|
||||
).filter { it.owningKey != ourKey }.map { initiateFlow(it) }
|
||||
|
||||
sessions.forEach {
|
||||
it.send(allMustSignStatus)
|
||||
|
||||
if (allMustSign) {
|
||||
stx = subFlow(CollectSignaturesFlow(stx , sessions))
|
||||
}
|
||||
}
|
||||
|
||||
return subFlow(FinalityFlow(stx, sessions))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(MoveFlow::class)
|
||||
class MoveHandler(val otherSession: FlowSession): FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
if (!serviceHub.myInfo.isLegalIdentity(otherSession.counterparty)) {
|
||||
|
||||
val requiresSignature = otherSession.receive(SignaturesRequired::class.java).unwrap { it }
|
||||
|
||||
if (requiresSignature == SignaturesRequired.ALL) {
|
||||
|
||||
subFlow(object : SignTransactionFlow(otherSession, encrypted = true) {
|
||||
override fun checkTransaction(stx: SignedTransaction) {
|
||||
val inputs = stx.tx.inputsStates.filterIsInstance<StateAndRef<BasicToken>>()
|
||||
val outputs = stx.tx.outputsOfType(BasicToken::class.java)
|
||||
|
||||
// a test condition we can use to trigger a signature failure
|
||||
require(!(inputs.any { serviceHub.identityService.wellKnownPartyFromAnonymous(it.state.data.holder)?.name == BOC_NAME } &&
|
||||
outputs.any { serviceHub.identityService.wellKnownPartyFromAnonymous(it.holder)?.name == BOB_NAME })){
|
||||
"Bank of Corda cannot move money to Bob"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
subFlow(ReceiveFinalityFlow(otherSideSession = otherSession))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user