Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ This document serves as the change log for the ONIXLabs Corda Core API.
- Moved to new extension file naming convention for maintainability.
- Added extensions to obtain single inputs, reference inputs and outputs from a `LedgerTransaction`.
- Added extension to cast `Iterable<StateAndRef<*>>` to `List<StateAndRef<T>>`.
- Added `ContractID` interface which automatically binds a contract ID to a contract class.
- Added `SignedCommandData` interface which defines a contract command that must include a signature.
- Added `VerifiedCommandData` interface which verifies a ledger transaction.
- Added `SignatureData` class, which represents a digital signature, and it's unsigned counterpart.

### Workflow

Expand Down
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ buildscript {
}

group 'io.onixlabs'
version '1.1.0'
version '1.2.0'

subprojects {
repositories {
Expand Down Expand Up @@ -93,10 +93,10 @@ task cleanLocal(type: Exec) {

task releaseLocal(type: GradleBuild) {
startParameter = gradle.startParameter.newInstance()
tasks = ['cleanLocal', 'clean', 'build', 'publishToMavenLocal']
tasks = ['clean', 'build', 'publishToMavenLocal']
}

task releasePublic(type: GradleBuild) {
startParameter = gradle.startParameter.newInstance()
tasks = ['cleanLocal', 'clean', 'build', 'publishToMavenLocal', 'publish']
tasks = ['clean', 'build', 'publishToMavenLocal', 'publish']
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.onixlabs.corda.core.contract

import net.corda.core.contracts.ContractClassName

/**
* Defines an interface which automatically binds a contract ID to a contract class.
*
* @property ID The ID of the contract.
*/
interface ContractID {
val ID: ContractClassName get() = this::class.java.enclosingClass.canonicalName
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,53 @@

package io.onixlabs.corda.core.contract

import net.corda.core.contracts.BelongsToContract
import net.corda.core.contracts.Contract
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.*
import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.LedgerTransaction
import java.security.PublicKey

/**
* Represents a dummy state and contract that will never be used.
* This exists so that Corda will load the contract into attachment storage.
* This exists for two reasons:
* 1. So that Corda will load the contract into attachment storage.
* 2. To test contract interface implementations locally.
*/
@Suppress("UNUSED")
internal class DummyContract : Contract {
override fun verify(tx: LedgerTransaction) = Unit

companion object : ContractID

@BelongsToContract(DummyContract::class)
class DummyState : ChainState {
override val previousStateRef: StateRef? get() = null
override val participants: List<AbstractParty> get() = emptyList()
data class DummyState(
override val participants: List<AbstractParty> = emptyList(),
override val previousStateRef: StateRef? = null
) : ChainState

override fun verify(tx: LedgerTransaction) {
val command = tx.commands.requireSingleCommand<DummyContractCommand>()
when (command.value) {
is DummyCommand -> command.value.verify(tx, command.signers.toSet())
else -> throw IllegalArgumentException("Unrecognised command: ${command.value}.")
}
}

interface DummyContractCommand : SignedCommandData, VerifiedCommandData

class DummyCommand(override val signature: SignatureData) : DummyContractCommand {

companion object {
internal const val CONTRACT_RULE_COMMAND_SIGNED =
"On dummy command, the command must be signed by the dummy state participant."
}

override fun verify(transaction: LedgerTransaction, signers: Set<PublicKey>) = requireThat {
val state = transaction.singleOutputOfType<DummyState>()
val command = transaction.singleCommandOfType<DummyCommand>()

val key = state.participants.single().owningKey
val signature = command.value.signature

CONTRACT_RULE_COMMAND_SIGNED using (signature.verify(key))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,35 @@ import net.corda.core.contracts.TransactionState
* Casts a [StateAndRef] of an unknown [ContractState] to a [StateAndRef] of type [T].
*
* @param T The underlying [ContractState] type to cast to.
* @param contractStateClass The [ContractState] class to cast to.
* @return Returns a [StateAndRef] of type [T].
* @throws ClassCastException if the unknown [ContractState] type cannot be cast to [T].
*/
inline fun <reified T> StateAndRef<*>.cast(): StateAndRef<T> where T : ContractState = with(state) {
StateAndRef(TransactionState(T::class.java.cast(data), contract, notary, encumbrance, constraint), ref)
fun <T> StateAndRef<*>.cast(contractStateClass: Class<T>): StateAndRef<T> where T : ContractState = with(state) {
StateAndRef(TransactionState(contractStateClass.cast(data), contract, notary, encumbrance, constraint), ref)
}

/**
* Casts a [StateAndRef] of an unknown [ContractState] to a [StateAndRef] of type [T].
*
* @param T The underlying [ContractState] type to cast to.
* @return Returns a [StateAndRef] of type [T].
* @throws ClassCastException if the unknown [ContractState] type cannot be cast to [T].
*/
inline fun <reified T> StateAndRef<*>.cast(): StateAndRef<T> where T : ContractState {
return cast(T::class.java)
}

/**
* Casts an iterable of [StateAndRef] of an unknown [ContractState] to a list of [StateAndRef] of type [T].
*
* @param T The underlying [ContractState] type to cast to.
* @param contractStateClass The [ContractState] class to cast to.
* @return Returns a list of [StateAndRef] of type [T].
* @throws ClassCastException if the unknown [ContractState] type cannot be cast to [T].
*/
fun <T> Iterable<StateAndRef<*>>.cast(contractStateClass: Class<T>): List<StateAndRef<T>> where T : ContractState {
return map { it.cast(contractStateClass) }
}

/**
Expand All @@ -39,5 +63,5 @@ inline fun <reified T> StateAndRef<*>.cast(): StateAndRef<T> where T : ContractS
* @throws ClassCastException if the unknown [ContractState] type cannot be cast to [T].
*/
inline fun <reified T> Iterable<StateAndRef<*>>.cast(): List<StateAndRef<T>> where T : ContractState {
return map { it.cast<T>() }
return cast(T::class.java)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.onixlabs.corda.core.contract

import net.corda.core.crypto.Crypto
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.sign
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.toBase64
import java.security.PrivateKey
import java.security.PublicKey
import java.util.*

/**
* Represents an array of unsigned bytes, and its signed equivalent.
*
* @property content The unsigned signature content.
* @property signature The digital signature representing the signed content.
*/
@CordaSerializable
data class SignatureData(private val content: ByteArray, private val signature: DigitalSignature) {

companion object {

/**
* Creates a signature from the specified content and private key.
*
* @param content The content to sign.
* @param privateKey The private key to sign the content.
* @return Returns a new signature containing the content and signed data.
*/
fun create(content: ByteArray, privateKey: PrivateKey): SignatureData {
val signature = privateKey.sign(content)
return SignatureData(content, signature)
}

/**
* Creates a signature from the specified content by resolving the signing key from the service hub.
*
* @param content The content to sign.
* @param publicKey The public key to resolve from the service hub.
* @param serviceHub The service hub to resolve the public key.
* @return Returns a new signature containing the content and signed data.
*/
fun create(content: ByteArray, publicKey: PublicKey, serviceHub: ServiceHub): SignatureData {
val signature = serviceHub.keyManagementService.sign(content, publicKey)
return SignatureData(content, signature.withoutKey())
}
}

/**
* Verifies the signature data using the specified public key.
*
* @param publicKey The public key to verify against the signature.
* @return Returns true if the public key was used to sign the data; otherwise, false.
*/
fun verify(publicKey: PublicKey): Boolean {
return Crypto.isValid(publicKey, signature.bytes, content)
}

/**
* Determines whether the specified object is equal to the current object.
*
* @param other The object to compare with the current object.
* @return Returns true if the specified object is equal to the current object; otherwise, false.
*/
override fun equals(other: Any?): Boolean {
return this === other || (other is SignatureData
&& content.contentEquals(other.content)
&& signature == other.signature)
}

/**
* Serves as the default hash function.
*
* @return Returns a hash code for the current object.
*/
override fun hashCode(): Int {
return Objects.hash(signature, content.contentHashCode())
}

/**
* Returns a string that represents the current object.
*
* @return Returns a string that represents the current object.
*/
override fun toString(): String = buildString {
appendln("Unsigned bytes: ${content.toBase64()}")
appendln("Signed bytes: ${signature.bytes.toBase64()}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.onixlabs.corda.core.contract

import net.corda.core.contracts.CommandData

/**
* Defines a contract command that must include a signature.
*
* @property signature The signature to include as a payload in the command.
*/
interface SignedCommandData : CommandData {
val signature: SignatureData
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.onixlabs.corda.core.contract

import net.corda.core.contracts.CommandData
import net.corda.core.transactions.LedgerTransaction
import java.security.PublicKey

/**
* Defines a contract command that can verify a ledger transaction.
*/
interface VerifiedCommandData : CommandData {

/**
* Verifies a ledger transaction.
*
* @param transaction The ledger transaction to verify.
* @param signers The list of signers expected to sign the transaction.
*/
fun verify(transaction: LedgerTransaction, signers: Set<PublicKey>)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.onixlabs.corda.core.contract

import net.corda.core.node.NotaryInfo
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.TestIdentity
import net.corda.testing.node.MockServices
import org.junit.jupiter.api.BeforeEach

abstract class ContractTest {

protected companion object {
private val cordapps = listOf("io.onixlabs.corda.core.contract")
private val contracts = listOf(DummyContract.ID)

fun keysOf(vararg identities: TestIdentity) = identities.map { it.publicKey }
}

private lateinit var _services: MockServices
protected val services: MockServices get() = _services

@BeforeEach
private fun setup() {
val networkParameters = testNetworkParameters(
minimumPlatformVersion = 8,
notaries = listOf(NotaryInfo(NOTARY.party, true))
)
_services = MockServices(cordapps, IDENTITY_A, networkParameters, IDENTITY_B, IDENTITY_C)
contracts.forEach { _services.addMockCordapp(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.onixlabs.corda.core.contract

import net.corda.core.crypto.SecureHash
import net.corda.testing.node.ledger
import org.junit.jupiter.api.Test

class DummyContractCommandTests : ContractTest() {

@Test
fun `On dummy command, the dummy state must be signed by the state participant`() {
services.ledger {
transaction {
val state = DummyContract.DummyState(listOf(IDENTITY_A.party))
val content = SecureHash.randomSHA256().bytes
val signature = SignatureData.create(content, IDENTITY_B.keyPair.private)
output(DummyContract.ID, state)
command(keysOf(IDENTITY_A), DummyContract.DummyCommand(signature))
failsWith(DummyContract.DummyCommand.CONTRACT_RULE_COMMAND_SIGNED)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.onixlabs.corda.core.contract

import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class DummyContractTests {

@Test
fun `DummyContract ID should be the canonical name of the DummyContract class`() {

// Arrange
val expected = "io.onixlabs.corda.core.contract.DummyContract"

// Act
val actual = DummyContract.ID

// Assert
assertEquals(expected, actual)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package io.onixlabs.corda.core.contract

import net.corda.core.identity.CordaX500Name
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.TestIdentity

val IDENTITY_A = TestIdentity(CordaX500Name("PartyA", "London", "GB"))
val IDENTITY_B = TestIdentity(CordaX500Name("PartyB", "New York", "US"))
val IDENTITY_C = TestIdentity(CordaX500Name("PartyC", "Paris", "FR"))
val IDENTITY_C = TestIdentity(CordaX500Name("PartyC", "Paris", "FR"))
val NOTARY = TestIdentity(DUMMY_NOTARY_NAME)