Skip to content

Commit e2cec3a

Browse files
Merge pull request #4 from onix-labs/feature-contract
Feature - Contract Command Signing
2 parents bba85d2 + a8e0044 commit e2cec3a

File tree

12 files changed

+262
-13
lines changed

12 files changed

+262
-13
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ This document serves as the change log for the ONIXLabs Corda Core API.
1313
- Moved to new extension file naming convention for maintainability.
1414
- Added extensions to obtain single inputs, reference inputs and outputs from a `LedgerTransaction`.
1515
- Added extension to cast `Iterable<StateAndRef<*>>` to `List<StateAndRef<T>>`.
16+
- Added `ContractID` interface which automatically binds a contract ID to a contract class.
17+
- Added `SignedCommandData` interface which defines a contract command that must include a signature.
18+
- Added `VerifiedCommandData` interface which verifies a ledger transaction.
19+
- Added `SignatureData` class, which represents a digital signature, and it's unsigned counterpart.
1620

1721
### Workflow
1822

build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ buildscript {
4242
}
4343

4444
group 'io.onixlabs'
45-
version '1.1.0'
45+
version '1.2.0'
4646

4747
subprojects {
4848
repositories {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.onixlabs.corda.core.contract
2+
3+
import net.corda.core.contracts.ContractClassName
4+
5+
/**
6+
* Defines an interface which automatically binds a contract ID to a contract class.
7+
*
8+
* @property ID The ID of the contract.
9+
*/
10+
interface ContractID {
11+
val ID: ContractClassName get() = this::class.java.enclosingClass.canonicalName
12+
}

onixlabs-corda-core-contract/src/main/kotlin/io/onixlabs/corda/core/contract/DummyContract.kt

+38-8
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,53 @@
1616

1717
package io.onixlabs.corda.core.contract
1818

19-
import net.corda.core.contracts.BelongsToContract
20-
import net.corda.core.contracts.Contract
21-
import net.corda.core.contracts.StateRef
19+
import net.corda.core.contracts.*
2220
import net.corda.core.identity.AbstractParty
2321
import net.corda.core.transactions.LedgerTransaction
22+
import java.security.PublicKey
2423

2524
/**
2625
* Represents a dummy state and contract that will never be used.
27-
* This exists so that Corda will load the contract into attachment storage.
26+
* This exists for two reasons:
27+
* 1. So that Corda will load the contract into attachment storage.
28+
* 2. To test contract interface implementations locally.
2829
*/
2930
@Suppress("UNUSED")
3031
internal class DummyContract : Contract {
31-
override fun verify(tx: LedgerTransaction) = Unit
32+
33+
companion object : ContractID
3234

3335
@BelongsToContract(DummyContract::class)
34-
class DummyState : ChainState {
35-
override val previousStateRef: StateRef? get() = null
36-
override val participants: List<AbstractParty> get() = emptyList()
36+
data class DummyState(
37+
override val participants: List<AbstractParty> = emptyList(),
38+
override val previousStateRef: StateRef? = null
39+
) : ChainState
40+
41+
override fun verify(tx: LedgerTransaction) {
42+
val command = tx.commands.requireSingleCommand<DummyContractCommand>()
43+
when (command.value) {
44+
is DummyCommand -> command.value.verify(tx, command.signers.toSet())
45+
else -> throw IllegalArgumentException("Unrecognised command: ${command.value}.")
46+
}
47+
}
48+
49+
interface DummyContractCommand : SignedCommandData, VerifiedCommandData
50+
51+
class DummyCommand(override val signature: SignatureData) : DummyContractCommand {
52+
53+
companion object {
54+
internal const val CONTRACT_RULE_COMMAND_SIGNED =
55+
"On dummy command, the command must be signed by the dummy state participant."
56+
}
57+
58+
override fun verify(transaction: LedgerTransaction, signers: Set<PublicKey>) = requireThat {
59+
val state = transaction.singleOutputOfType<DummyState>()
60+
val command = transaction.singleCommandOfType<DummyCommand>()
61+
62+
val key = state.participants.single().owningKey
63+
val signature = command.value.signature
64+
65+
CONTRACT_RULE_COMMAND_SIGNED using (signature.verify(key))
66+
}
3767
}
3868
}

onixlabs-corda-core-contract/src/main/kotlin/io/onixlabs/corda/core/contract/Extensions.StateAndRef.kt

+27-3
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,35 @@ import net.corda.core.contracts.TransactionState
2424
* Casts a [StateAndRef] of an unknown [ContractState] to a [StateAndRef] of type [T].
2525
*
2626
* @param T The underlying [ContractState] type to cast to.
27+
* @param contractStateClass The [ContractState] class to cast to.
2728
* @return Returns a [StateAndRef] of type [T].
2829
* @throws ClassCastException if the unknown [ContractState] type cannot be cast to [T].
2930
*/
30-
inline fun <reified T> StateAndRef<*>.cast(): StateAndRef<T> where T : ContractState = with(state) {
31-
StateAndRef(TransactionState(T::class.java.cast(data), contract, notary, encumbrance, constraint), ref)
31+
fun <T> StateAndRef<*>.cast(contractStateClass: Class<T>): StateAndRef<T> where T : ContractState = with(state) {
32+
StateAndRef(TransactionState(contractStateClass.cast(data), contract, notary, encumbrance, constraint), ref)
33+
}
34+
35+
/**
36+
* Casts a [StateAndRef] of an unknown [ContractState] to a [StateAndRef] of type [T].
37+
*
38+
* @param T The underlying [ContractState] type to cast to.
39+
* @return Returns a [StateAndRef] of type [T].
40+
* @throws ClassCastException if the unknown [ContractState] type cannot be cast to [T].
41+
*/
42+
inline fun <reified T> StateAndRef<*>.cast(): StateAndRef<T> where T : ContractState {
43+
return cast(T::class.java)
44+
}
45+
46+
/**
47+
* Casts an iterable of [StateAndRef] of an unknown [ContractState] to a list of [StateAndRef] of type [T].
48+
*
49+
* @param T The underlying [ContractState] type to cast to.
50+
* @param contractStateClass The [ContractState] class to cast to.
51+
* @return Returns a list of [StateAndRef] of type [T].
52+
* @throws ClassCastException if the unknown [ContractState] type cannot be cast to [T].
53+
*/
54+
fun <T> Iterable<StateAndRef<*>>.cast(contractStateClass: Class<T>): List<StateAndRef<T>> where T : ContractState {
55+
return map { it.cast(contractStateClass) }
3256
}
3357

3458
/**
@@ -39,5 +63,5 @@ inline fun <reified T> StateAndRef<*>.cast(): StateAndRef<T> where T : ContractS
3963
* @throws ClassCastException if the unknown [ContractState] type cannot be cast to [T].
4064
*/
4165
inline fun <reified T> Iterable<StateAndRef<*>>.cast(): List<StateAndRef<T>> where T : ContractState {
42-
return map { it.cast<T>() }
66+
return cast(T::class.java)
4367
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.onixlabs.corda.core.contract
2+
3+
import net.corda.core.crypto.Crypto
4+
import net.corda.core.crypto.DigitalSignature
5+
import net.corda.core.crypto.sign
6+
import net.corda.core.node.ServiceHub
7+
import net.corda.core.serialization.CordaSerializable
8+
import net.corda.core.utilities.toBase64
9+
import java.security.PrivateKey
10+
import java.security.PublicKey
11+
import java.util.*
12+
13+
/**
14+
* Represents an array of unsigned bytes, and its signed equivalent.
15+
*
16+
* @property content The unsigned signature content.
17+
* @property signature The digital signature representing the signed content.
18+
*/
19+
@CordaSerializable
20+
data class SignatureData(private val content: ByteArray, private val signature: DigitalSignature) {
21+
22+
companion object {
23+
fun create(content: ByteArray, privateKey: PrivateKey): SignatureData {
24+
val signature = privateKey.sign(content)
25+
return SignatureData(content, signature)
26+
}
27+
28+
fun create(content: ByteArray, publicKey: PublicKey, serviceHub: ServiceHub): SignatureData {
29+
val signature = serviceHub.keyManagementService.sign(content, publicKey)
30+
return SignatureData(content, signature.withoutKey())
31+
}
32+
}
33+
34+
/**
35+
* Verifies the signature data using the specified public key.
36+
*
37+
* @param publicKey The public key to verify against the signature.
38+
* @return Returns true if the public key was used to sign the data; otherwise, false.
39+
*/
40+
fun verify(publicKey: PublicKey): Boolean {
41+
return Crypto.isValid(publicKey, signature.bytes, content)
42+
}
43+
44+
/**
45+
* Determines whether the specified object is equal to the current object.
46+
*
47+
* @param other The object to compare with the current object.
48+
* @return Returns true if the specified object is equal to the current object; otherwise, false.
49+
*/
50+
override fun equals(other: Any?): Boolean {
51+
return this === other || (other is SignatureData
52+
&& content.contentEquals(other.content)
53+
&& signature == other.signature)
54+
}
55+
56+
/**
57+
* Serves as the default hash function.
58+
*
59+
* @return Returns a hash code for the current object.
60+
*/
61+
override fun hashCode(): Int {
62+
return Objects.hash(signature, content.contentHashCode())
63+
}
64+
65+
/**
66+
* Returns a string that represents the current object.
67+
*
68+
* @return Returns a string that represents the current object.
69+
*/
70+
override fun toString(): String = buildString {
71+
appendln("Unsigned bytes: ${content.toBase64()}")
72+
appendln("Signed bytes: ${signature.bytes.toBase64()}")
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.onixlabs.corda.core.contract
2+
3+
import net.corda.core.contracts.CommandData
4+
5+
/**
6+
* Defines a contract command that must include a signature.
7+
*
8+
* @property signature The signature to include as a payload in the command.
9+
*/
10+
interface SignedCommandData : CommandData {
11+
val signature: SignatureData
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.onixlabs.corda.core.contract
2+
3+
import net.corda.core.contracts.CommandData
4+
import net.corda.core.transactions.LedgerTransaction
5+
import java.security.PublicKey
6+
7+
/**
8+
* Defines a contract command that can verify a ledger transaction.
9+
*/
10+
interface VerifiedCommandData : CommandData {
11+
12+
/**
13+
* Verifies a ledger transaction.
14+
*
15+
* @param transaction The ledger transaction to verify.
16+
* @param signers The list of signers expected to sign the transaction.
17+
*/
18+
fun verify(transaction: LedgerTransaction, signers: Set<PublicKey>)
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package io.onixlabs.corda.core.contract
2+
3+
import net.corda.core.node.NotaryInfo
4+
import net.corda.testing.common.internal.testNetworkParameters
5+
import net.corda.testing.core.TestIdentity
6+
import net.corda.testing.node.MockServices
7+
import org.junit.jupiter.api.BeforeEach
8+
9+
abstract class ContractTest {
10+
11+
protected companion object {
12+
private val cordapps = listOf("io.onixlabs.corda.core.contract")
13+
private val contracts = listOf(DummyContract.ID)
14+
15+
fun keysOf(vararg identities: TestIdentity) = identities.map { it.publicKey }
16+
}
17+
18+
private lateinit var _services: MockServices
19+
protected val services: MockServices get() = _services
20+
21+
@BeforeEach
22+
private fun setup() {
23+
val networkParameters = testNetworkParameters(
24+
minimumPlatformVersion = 8,
25+
notaries = listOf(NotaryInfo(NOTARY.party, true))
26+
)
27+
_services = MockServices(cordapps, IDENTITY_A, networkParameters, IDENTITY_B, IDENTITY_C)
28+
contracts.forEach { _services.addMockCordapp(it) }
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.onixlabs.corda.core.contract
2+
3+
import net.corda.core.crypto.SecureHash
4+
import net.corda.testing.node.ledger
5+
import org.junit.jupiter.api.Test
6+
7+
class DummyContractCommandTests : ContractTest() {
8+
9+
@Test
10+
fun `On dummy command, the dummy state must be signed by the state participant`() {
11+
services.ledger {
12+
transaction {
13+
val state = DummyContract.DummyState(listOf(IDENTITY_A.party))
14+
val content = SecureHash.randomSHA256().bytes
15+
val signature = SignatureData.create(content, IDENTITY_B.keyPair.private)
16+
output(DummyContract.ID, state)
17+
command(keysOf(IDENTITY_A), DummyContract.DummyCommand(signature))
18+
failsWith(DummyContract.DummyCommand.CONTRACT_RULE_COMMAND_SIGNED)
19+
}
20+
}
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.onixlabs.corda.core.contract
2+
3+
import org.junit.jupiter.api.Test
4+
import kotlin.test.assertEquals
5+
6+
class DummyContractTests {
7+
8+
@Test
9+
fun `DummyContract ID should be the canonical name of the DummyContract class`() {
10+
11+
// Arrange
12+
val expected = "io.onixlabs.corda.core.contract.DummyContract"
13+
14+
// Act
15+
val actual = DummyContract.ID
16+
17+
// Assert
18+
assertEquals(expected, actual)
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package io.onixlabs.corda.core.contract
22

33
import net.corda.core.identity.CordaX500Name
4+
import net.corda.testing.core.DUMMY_NOTARY_NAME
45
import net.corda.testing.core.TestIdentity
56

67
val IDENTITY_A = TestIdentity(CordaX500Name("PartyA", "London", "GB"))
78
val IDENTITY_B = TestIdentity(CordaX500Name("PartyB", "New York", "US"))
8-
val IDENTITY_C = TestIdentity(CordaX500Name("PartyC", "Paris", "FR"))
9+
val IDENTITY_C = TestIdentity(CordaX500Name("PartyC", "Paris", "FR"))
10+
val NOTARY = TestIdentity(DUMMY_NOTARY_NAME)

0 commit comments

Comments
 (0)