Skip to content

Commit b4d49b8

Browse files
Merge pull request #5 from onix-labs/main
1.2.0
2 parents da623ea + 16398e6 commit b4d49b8

File tree

12 files changed

+280
-15
lines changed

12 files changed

+280
-15
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

+3-3
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 {
@@ -93,10 +93,10 @@ task cleanLocal(type: Exec) {
9393

9494
task releaseLocal(type: GradleBuild) {
9595
startParameter = gradle.startParameter.newInstance()
96-
tasks = ['cleanLocal', 'clean', 'build', 'publishToMavenLocal']
96+
tasks = ['clean', 'build', 'publishToMavenLocal']
9797
}
9898

9999
task releasePublic(type: GradleBuild) {
100100
startParameter = gradle.startParameter.newInstance()
101-
tasks = ['cleanLocal', 'clean', 'build', 'publishToMavenLocal', 'publish']
101+
tasks = ['clean', 'build', 'publishToMavenLocal', 'publish']
102102
}
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,90 @@
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+
24+
/**
25+
* Creates a signature from the specified content and private key.
26+
*
27+
* @param content The content to sign.
28+
* @param privateKey The private key to sign the content.
29+
* @return Returns a new signature containing the content and signed data.
30+
*/
31+
fun create(content: ByteArray, privateKey: PrivateKey): SignatureData {
32+
val signature = privateKey.sign(content)
33+
return SignatureData(content, signature)
34+
}
35+
36+
/**
37+
* Creates a signature from the specified content by resolving the signing key from the service hub.
38+
*
39+
* @param content The content to sign.
40+
* @param publicKey The public key to resolve from the service hub.
41+
* @param serviceHub The service hub to resolve the public key.
42+
* @return Returns a new signature containing the content and signed data.
43+
*/
44+
fun create(content: ByteArray, publicKey: PublicKey, serviceHub: ServiceHub): SignatureData {
45+
val signature = serviceHub.keyManagementService.sign(content, publicKey)
46+
return SignatureData(content, signature.withoutKey())
47+
}
48+
}
49+
50+
/**
51+
* Verifies the signature data using the specified public key.
52+
*
53+
* @param publicKey The public key to verify against the signature.
54+
* @return Returns true if the public key was used to sign the data; otherwise, false.
55+
*/
56+
fun verify(publicKey: PublicKey): Boolean {
57+
return Crypto.isValid(publicKey, signature.bytes, content)
58+
}
59+
60+
/**
61+
* Determines whether the specified object is equal to the current object.
62+
*
63+
* @param other The object to compare with the current object.
64+
* @return Returns true if the specified object is equal to the current object; otherwise, false.
65+
*/
66+
override fun equals(other: Any?): Boolean {
67+
return this === other || (other is SignatureData
68+
&& content.contentEquals(other.content)
69+
&& signature == other.signature)
70+
}
71+
72+
/**
73+
* Serves as the default hash function.
74+
*
75+
* @return Returns a hash code for the current object.
76+
*/
77+
override fun hashCode(): Int {
78+
return Objects.hash(signature, content.contentHashCode())
79+
}
80+
81+
/**
82+
* Returns a string that represents the current object.
83+
*
84+
* @return Returns a string that represents the current object.
85+
*/
86+
override fun toString(): String = buildString {
87+
appendln("Unsigned bytes: ${content.toBase64()}")
88+
appendln("Signed bytes: ${signature.bytes.toBase64()}")
89+
}
90+
}
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)