New separate project for contracts. Canonicalization.

This commit is contained in:
Sofus Mortensen 2016-02-10 21:10:30 +01:00 committed by Mike Hearn
parent 8bd54dfc74
commit cbfcac994a
3 changed files with 229 additions and 2 deletions

View File

@ -1,3 +1,12 @@
import com.google.common.io.ByteStreams
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.nio.file.attribute.FileTime
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import java.util.zip.ZipFile
group 'com.r3cev.prototyping'
version '1.0-SNAPSHOT'
@ -26,7 +35,9 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// Dokka (JavaDoc equivalent for Kotlin) download is huge so just comment this out for now.
// classpath "org.jetbrains.dokka:dokka-gradle-plugin:0.9.6"
//classpath "org.jetbrains.dokka:dokka-gradle-plugin:0.9.6"
classpath "com.google.guava:guava:19.0"
}
}
@ -117,3 +128,59 @@ task runDemoSeller(type: JavaExec, dependsOn: ':classes') {
args = ['--dir=seller', '--fake-trade-with=localhost', '--network-address=localhost:31338',
'--timestamper-identity-file=buyer/identity-public', '--timestamper-address=localhost']
}
class CanonicalizerPlugin implements Plugin<Project> {
void apply(Project project) {
project.getTasks().getByName('jar').doLast() {
def zipPath = (String) project.jar.archivePath
def destPath = Files.createTempFile("processzip", null)
def zeroTime = FileTime.fromMillis(0)
def input = new ZipFile(zipPath)
def entries = input.entries()
def output = new ZipOutputStream(new FileOutputStream(destPath.toFile()))
output.setMethod(ZipOutputStream.STORED)
while (entries.hasMoreElements()) {
def entry = entries.nextElement()
def newEntry = new ZipEntry( entry.name )
newEntry.setLastModifiedTime(zeroTime)
newEntry.setCreationTime(zeroTime)
newEntry.compressedSize = -1
newEntry.size = entry.size
newEntry.crc = entry.crc
output.putNextEntry(newEntry)
ByteStreams.copy(input.getInputStream(entry), output)
output.closeEntry()
}
output.close()
input.close()
Files.move(destPath, Paths.get(zipPath), StandardCopyOption.REPLACE_EXISTING)
}
}
}
project(':contracts') {
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: CanonicalizerPlugin
repositories {
mavenCentral()
}
dependencies {
compile rootProject
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
* set forth therein.
*
* All other rights reserved.
*/
package contracts
import core.*
import core.crypto.NullPublicKey
import core.crypto.SecureHash
import core.crypto.toStringShort
import core.utilities.Emoji
import java.security.PublicKey
import java.time.Instant
/**
* This is an ultra-trivial implementation of commercial paper, which is essentially a simpler version of a corporate
* bond. It can be seen as a company-specific currency. A company issues CP with a particular face value, say $100,
* but sells it for less, say $90. The paper can be redeemed for cash at a given date in the future. Thus this example
* would have a 10% interest rate with a single repayment. Commercial paper is often rolled over (the maturity date
* is adjusted as if the paper was redeemed and immediately repurchased, but without having to front the cash).
*
* This contract is not intended to realistically model CP. It is here only to act as a next step up above cash in
* the prototyping phase. It is thus very incomplete.
*
* Open issues:
* - In this model, you cannot merge or split CP. Can you do this normally? We could model CP as a specialised form
* of cash, or reuse some of the cash code? Waiting on response from Ayoub and Rajar about whether CP can always
* be split/merged or only in secondary markets. Even if current systems can't do this, would it be a desirable
* feature to have anyway?
* - The funding steps of CP is totally ignored in this model.
* - No attention is paid to the existing roles of custodians, funding banks, etc.
* - There are regional variations on the CP concept, for instance, American CP requires a special "CUSIP number"
* which may need to be tracked. That, in turn, requires validation logic (there is a bean validator that knows how
* to do this in the Apache BVal project).
*/
val CP_PROGRAM_ID = SecureHash.sha256("replace-me-later-with-bytecode-hash")
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
class CommercialPaper : Contract {
// TODO: should reference the content of the legal agreement, not its URI
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper")
data class State(
val issuance: PartyReference,
override val owner: PublicKey,
val faceValue: Amount,
val maturityDate: Instant
) : OwnableState, ICommercialPaperState {
override val programRef = CP_PROGRAM_ID
fun withoutOwner() = copy(owner = NullPublicKey)
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by ${owner.toStringShort()})"
// Although kotlin is smart enough not to need these, as we are using the ICommercialPaperState, we need to declare them explicitly for use later,
override fun withOwner(newOwner: PublicKey): ICommercialPaperState = copy(owner = newOwner)
override fun withIssuance(newIssuance: PartyReference): ICommercialPaperState = copy(issuance = newIssuance)
override fun withFaceValue(newFaceValue: Amount): ICommercialPaperState = copy(faceValue = newFaceValue)
override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate)
}
interface Commands : CommandData {
class Move : TypeOnlyCommandData(), Commands
class Redeem : TypeOnlyCommandData(), Commands
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
// However, nothing in the platform enforces that uniqueness: it's up to the issuer.
class Issue : TypeOnlyCommandData(), Commands
}
override fun verify(tx: TransactionForVerification) {
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
val groups = tx.groupStates<State>() { it.withoutOwner() }
// There are two possible things that can be done with this CP. The first is trading it. The second is redeeming
// it for cash on or after the maturity date.
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
val timestamp: TimestampCommand? = tx.getTimestampBy(DummyTimestampingAuthority.identity)
for (group in groups) {
when (command.value) {
is Commands.Move -> {
val input = group.inputs.single()
requireThat {
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
"the state is propagated" by (group.outputs.size == 1)
}
}
is Commands.Redeem -> {
val input = group.inputs.single()
val received = tx.outStates.sumCashBy(input.owner)
val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped")
requireThat {
"the paper must have matured" by (time > input.maturityDate)
"the received amount equals the face value" by (received == input.faceValue)
"the paper must be destroyed" by group.outputs.isEmpty()
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
}
}
is Commands.Issue -> {
val output = group.outputs.single()
val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
requireThat {
// Don't allow people to issue commercial paper under other entities identities.
"the issuance is signed by the claimed issuer of the paper" by
(command.signers.contains(output.issuance.party.owningKey))
"the face value is not zero" by (output.faceValue.pennies > 0)
"the maturity date is not in the past" by (time < output.maturityDate)
// Don't allow an existing CP state to be replaced by this issuance.
"there is no input state" by group.inputs.isEmpty()
}
}
// TODO: Think about how to evolve contracts over time with new commands.
else -> throw IllegalArgumentException("Unrecognised command")
}
}
}
/**
* Returns a transaction that issues commercial paper, owned by the issuing parties key. Does not update
* an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction
* at the moment: this restriction is not fundamental and may be lifted later.
*/
fun generateIssue(issuance: PartyReference, faceValue: Amount, maturityDate: Instant): TransactionBuilder {
val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate)
return TransactionBuilder().withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
}
/**
* Updates the given partial transaction with an input/output/command to reassign ownership of the paper.
*/
fun generateMove(tx: TransactionBuilder, paper: StateAndRef<State>, newOwner: PublicKey) {
tx.addInputState(paper.ref)
tx.addOutputState(paper.state.copy(owner = newOwner))
tx.addCommand(Commands.Move(), paper.state.owner)
}
/**
* Intended to be called by the issuer of some commercial paper, when an owner has notified us that they wish
* to redeem the paper. We must therefore send enough money to the key that owns the paper to satisfy the face
* value, and then ensure the paper is removed from the ledger.
*
* @throws InsufficientBalanceException if the wallet doesn't contain enough money to pay the redeemer
*/
@Throws(InsufficientBalanceException::class)
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) {
// Add the cash movement using the states in our wallet.
Cash().generateSpend(tx, paper.state.faceValue, paper.state.owner, wallet)
tx.addInputState(paper.ref)
tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.owner)
}
}

View File

@ -1,2 +1,2 @@
rootProject.name = 'r3prototyping'
include 'contracts'