Merged master into dynamic-loading

This commit is contained in:
Sofus Mortensen 2016-03-05 21:45:56 +01:00
commit be52c5f1b0
50 changed files with 825 additions and 254 deletions

View File

@ -3,7 +3,7 @@
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="core.node.TraderDemoKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar -Dco.paralleluniverse.fibers.verifyInstrumentation" />
<option name="PROGRAM_PARAMETERS" value="--dir=seller --fake-trade-with=localhost --network-address=localhost:31338 --timestamper-identity-file=buyer/identity-public --timestamper-address=localhost" />
<option name="PROGRAM_PARAMETERS" value="--dir=seller --fake-trade-with=localhost --network-address=localhost:31327 --timestamper-identity-file=buyer/identity-public --timestamper-address=localhost" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />

View File

@ -12,7 +12,8 @@ allprojects {
buildscript {
ext.kotlin_version = '1.0.0'
ext.quasar_version = '0.7.4'
// TODO: Reset to 0.7.5 when released. We need the snapshot for a TLS serialization related fix.
ext.quasar_version = '0.7.5-SNAPSHOT'
ext.asm_version = '0.5.3'
ext.artemis_version = '1.2.0'
ext.jetty_version = '9.3.7.v20160115'
@ -28,7 +29,7 @@ buildscript {
repositories {
// mavenLocal()
mavenLocal()
mavenCentral()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
@ -67,12 +68,7 @@ dependencies {
// JOpt: for command line flags.
compile "net.sf.jopt-simple:jopt-simple:4.9"
// Kryo: object graph serialization.
compile "com.esotericsoftware:kryo:3.0.3"
compile "de.javakaffee:kryo-serializers:0.37"
// Quasar: for the bytecode rewriting for state machines.
compile("co.paralleluniverse:quasar-core:${quasar_version}:jdk8")
quasar("co.paralleluniverse:quasar-core:${quasar_version}:jdk8@jar")
// Artemis: for reliable p2p message queues.

View File

@ -67,6 +67,12 @@ apply plugin: CanonicalizerPlugin
repositories {
mavenCentral()
mavenLocal()
mavenCentral()
jcenter()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
}
}
dependencies {

View File

@ -7,7 +7,6 @@ apply plugin: 'kotlin'
buildscript {
repositories {
mavenCentral()
jcenter()
}
dependencies {
// Dokka (JavaDoc equivalent for Kotlin) download is a big download and also
@ -22,7 +21,12 @@ buildscript {
// apply plugin: 'org.jetbrains.dokka'
repositories {
mavenLocal()
mavenCentral()
jcenter()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
}
}
dependencies {
@ -41,6 +45,10 @@ dependencies {
// RxJava: observable streams of events.
compile "io.reactivex:rxkotlin:0.40.1"
// Kryo: object graph serialization.
compile "com.esotericsoftware:kryo:3.0.3"
compile "de.javakaffee:kryo-serializers:0.37"
// Quasar: for the bytecode rewriting for state machines.
compile "co.paralleluniverse:quasar-core:${quasar_version}:jdk8"

View File

@ -12,7 +12,9 @@ import core.crypto.SecureHash
import core.crypto.toStringShort
import core.serialization.OpaqueBytes
import core.serialization.serialize
import java.io.FileNotFoundException
import java.io.InputStream
import java.io.OutputStream
import java.security.PublicKey
import java.time.Duration
import java.time.Instant
@ -169,4 +171,25 @@ class UnknownContractException : Exception()
interface Attachment : NamedByHash {
fun open(): InputStream
fun openAsJAR() = JarInputStream(open())
/**
* Finds the named file case insensitively and copies it to the output stream.
*
* @throws FileNotFoundException if the given path doesn't exist in the attachment.
*/
fun extractFile(path: String, outputTo: OutputStream) {
val p = path.toLowerCase()
openAsJAR().use { jar ->
while (true) {
val e = jar.nextJarEntry ?: break
// TODO: Normalise path separators here for more platform independence, as zip doesn't mandate a type.
if (e.name.toLowerCase() == p) {
jar.copyTo(outputTo)
return
}
jar.closeEntry()
}
}
throw FileNotFoundException()
}
}

View File

@ -0,0 +1,61 @@
/*
* 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 core
import core.crypto.SecureHash
import java.util.*
import java.util.concurrent.Callable
/**
* Given a map of transaction id to [SignedTransaction], performs a breadth first search of the dependency graph from
* the starting point down in order to find transactions that match the given query criteria.
*
* Currently, only one kind of query is supported: find any transaction that contains a command of the given type.
*
* In future, this should support restricting the search by time, and other types of useful query.
*
* TODO: Write unit tests for this.
*/
class TransactionGraphSearch(val transactions: Map<SecureHash, SignedTransaction>,
val startPoints: List<WireTransaction>) : Callable<List<WireTransaction>> {
class Query(
val withCommandOfType: Class<out CommandData>? = null
)
var query: Query = Query()
override fun call(): List<WireTransaction> {
val q = query
val next = ArrayList<SecureHash>()
next += startPoints.flatMap { it.inputs.map { it.txhash } }
val results = ArrayList<WireTransaction>()
while (next.isNotEmpty()) {
val hash = next.removeAt(next.lastIndex)
val tx = transactions[hash]?.tx ?: continue
if (q.matches(tx))
results += tx
next += tx.inputs.map { it.txhash }
}
return results
}
private fun Query.matches(tx: WireTransaction): Boolean {
if (withCommandOfType != null) {
if (tx.commands.any { it.data.javaClass.isAssignableFrom(withCommandOfType) })
return true
}
return false
}
}

View File

@ -14,6 +14,8 @@ import java.util.*
class TransactionResolutionException(val hash: SecureHash) : Exception()
class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
// TODO: Consider moving this out of the core module and providing a different way for unit tests to test contracts.
/**
* A TransactionGroup defines a directed acyclic graph of transactions that can be resolved with each other and then
* verified. Successful verification does not imply the non-existence of other conflicting transactions: simply that
@ -49,7 +51,7 @@ class TransactionGroup(val transactions: Set<LedgerTransaction>, val nonVerified
// Look up the output in that transaction by index.
inputs.add(ltx.outputs[ref.index])
}
resolved.add(TransactionForVerification(inputs, tx.outputs, tx.commands, tx.hash))
resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.hash))
}
for (tx in resolved)
@ -62,12 +64,17 @@ class TransactionGroup(val transactions: Set<LedgerTransaction>, val nonVerified
/** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */
data class TransactionForVerification(val inStates: List<ContractState>,
val outStates: List<ContractState>,
val attachments: List<Attachment>,
val commands: List<AuthenticatedObject<CommandData>>,
val origHash: SecureHash) {
override fun hashCode() = origHash.hashCode()
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
/**
* Runs the contracts for this transaction.
*
* TODO: Move this out of the core data structure definitions, once unit tests are more cleanly separated.
*
* @throws TransactionVerificationException if a contract throws an exception, the original is in the cause field
* @throws IllegalStateException if a state refers to an unknown contract.
*/
@ -77,6 +84,7 @@ data class TransactionForVerification(val inStates: List<ContractState>,
// throws an exception, the entire transaction is invalid.
val programHashes = (inStates.map { it.programRef } + outStates.map { it.programRef }).toSet()
for (hash in programHashes) {
// TODO: Change this interface to ensure that attachment JARs are put on the classpath before execution.
val program: Contract = programMap[hash]
try {
program.verify(this)

View File

@ -30,7 +30,7 @@ import java.util.*
* Views of a transaction as it progresses through the pipeline, from bytes loaded from disk/network to the object
* tree passed into a contract.
*
* SignedWireTransaction wraps a serialized WireTransaction. It contains one or more ECDSA signatures, each one from
* SignedTransaction wraps a serialized WireTransaction. It contains one or more ECDSA signatures, each one from
* a public key that is mentioned inside a transaction command.
*
* WireTransaction is a transaction in a form ready to be serialised/unserialised. A WireTransaction can be hashed
@ -51,11 +51,13 @@ import java.util.*
* All the above refer to inputs using a (txhash, output index) pair.
*
* TransactionForVerification is the same as LedgerTransaction but with the input states looked up from a local
* database and replaced with the real objects. TFV is the form that is finally fed into the contracts.
* database and replaced with the real objects. Likewise, attachments are fully resolved at this point.
* TFV is the form that is finally fed into the contracts.
*/
/** Transaction ready for serialisation, without any signatures attached. */
data class WireTransaction(val inputs: List<StateRef>,
val attachments: List<SecureHash>,
val outputs: List<ContractState>,
val commands: List<Command>) : NamedByHash {
@ -72,14 +74,6 @@ data class WireTransaction(val inputs: List<StateRef>,
}
}
fun toLedgerTransaction(identityService: IdentityService): LedgerTransaction {
val authenticatedArgs = commands.map {
val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) }
AuthenticatedObject(it.pubkeys, institutions, it.data)
}
return LedgerTransaction(inputs, outputs, authenticatedArgs, id)
}
/** Serialises and returns this transaction as a [SignedTransaction] with no signatures attached. */
fun toSignedTransaction(withSigs: List<DigitalSignature.WithKey>): SignedTransaction {
return SignedTransaction(serialized, withSigs)
@ -98,9 +92,10 @@ data class WireTransaction(val inputs: List<StateRef>,
override fun toString(): String {
val buf = StringBuilder()
buf.appendln("Transaction:")
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output")
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output")
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
return buf.toString()
}
}
@ -151,15 +146,6 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
return missing
}
/**
* Calls [verify] to check all required signatures are present, and then uses the passed [IdentityService] to call
* [WireTransaction.toLedgerTransaction] to look up well known identities from pubkeys.
*/
fun verifyToLedgerTransaction(identityService: IdentityService): LedgerTransaction {
verify()
return tx.toLedgerTransaction(identityService)
}
/** Returns the same transaction but with an additional (unchecked) signature */
fun withAdditionalSignature(sig: DigitalSignature.WithKey) = copy(sigs = sigs + sig)
@ -169,6 +155,7 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
/** A mutable transaction that's in the process of being built, before all signatures are present. */
class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf(),
private val attachments: MutableList<SecureHash> = arrayListOf(),
private val outputs: MutableList<ContractState> = arrayListOf(),
private val commands: MutableList<Command> = arrayListOf()) {
@ -254,7 +241,8 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
currentSigs.add(sig)
}
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(outputs), ArrayList(commands))
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments),
ArrayList(outputs), ArrayList(commands))
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction {
if (checkSufficientSignatures) {
@ -272,6 +260,11 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
inputs.add(ref)
}
fun addAttachment(attachment: Attachment) {
check(currentSigs.isEmpty())
attachments.add(attachment.id)
}
fun addOutputState(state: ContractState) {
check(currentSigs.isEmpty())
outputs.add(state)
@ -291,16 +284,21 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
fun inputStates(): List<StateRef> = ArrayList(inputs)
fun outputStates(): List<ContractState> = ArrayList(outputs)
fun commands(): List<Command> = ArrayList(commands)
fun attachments(): List<SecureHash> = ArrayList(attachments)
}
/**
* A LedgerTransaction wraps the data needed to calculate one or more successor states from a set of input states.
* It is the first step after extraction from a WireTransaction. The signatures at this point have been lined up
* with the commands from the wire, and verified/looked up.
*
* TODO: This class needs a bit more thought. Should inputs be fully resolved by this point too?
*/
data class LedgerTransaction(
/** The input states which will be consumed/invalidated by the execution of this transaction. */
val inputs: List<StateRef>,
/** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */
val attachments: List<Attachment>,
/** The states that will be generated by the execution of this transaction. */
val outputs: List<ContractState>,
/** Arbitrary data passed to the program of each input state. */
@ -312,7 +310,7 @@ data class LedgerTransaction(
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as T, StateRef(hash, index))
fun toWireTransaction(): WireTransaction {
val wtx = WireTransaction(inputs, outputs, commands.map { Command(it.value, it.signers) })
val wtx = WireTransaction(inputs, attachments.map { it.id }, outputs, commands.map { Command(it.value, it.signers) })
check(wtx.serialize().hash == hash)
return wtx
}

View File

@ -8,10 +8,12 @@
package core
import com.google.common.io.ByteStreams
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import com.google.common.util.concurrent.SettableFuture
import org.slf4j.Logger
import java.io.BufferedInputStream
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
@ -21,6 +23,7 @@ import java.time.temporal.Temporal
import java.util.concurrent.Executor
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
import java.util.zip.ZipInputStream
import kotlin.concurrent.withLock
import kotlin.reflect.KProperty
@ -121,4 +124,32 @@ class TransientProperty<T>(private val initializer: () -> T) {
v = initializer()
return v!!
}
}
}
/**
* Given a path to a zip file, extracts it to the given directory.
*/
fun extractZipFile(zipPath: Path, toPath: Path) {
if (!Files.exists(toPath))
Files.createDirectories(toPath)
ZipInputStream(BufferedInputStream(Files.newInputStream(zipPath))).use { zip ->
while (true) {
val e = zip.nextEntry ?: break
val outPath = toPath.resolve(e.name)
// Security checks: we should reject a zip that contains tricksy paths that try to escape toPath.
if (!outPath.normalize().startsWith(toPath))
throw IllegalStateException("ZIP contained a path that resolved incorrectly: ${e.name}")
if (e.isDirectory) {
Files.createDirectories(outPath)
continue
}
Files.newOutputStream(outPath).use { out ->
ByteStreams.copy(zip, out)
}
zip.closeEntry()
}
}
}

View File

@ -32,7 +32,7 @@ sealed class SecureHash private constructor(bits: ByteArray) : OpaqueBytes(bits)
fun parse(str: String) = BaseEncoding.base16().decode(str.toUpperCase()).let {
when (it.size) {
32 -> SHA256(it)
else -> throw IllegalArgumentException("Provided string is not 32 bytes in base 16 (hex): $str")
else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str")
}
}

View File

@ -30,17 +30,23 @@ arguments to the verify function. Each command has a list of **public keys** ass
that the transaction is signed by every key listed in the commands before the contracts start to execute. Public keys
may be random/identityless for privacy, or linked to a well known legal identity via a *public key infrastructure* (PKI).
Note that there is nothing that explicitly binds together specific inputs, outputs or commands. Instead it's up to the
contract code to interpret the pieces inside the transaction and ensure they fit together correctly. This is done to
maximise flexibility for the contract developer.
Commands are always embedded inside a transaction. Sometimes, there's a larger piece of data that can be reused across
many different transactions. For this use case, we have **attachments**. Every transaction can refer to zero or more
attachments by hash. Attachments are always ZIP/JAR files, which may contain arbitrary content. Contract code can then
access the attachments by opening them as a JarInputStream (this is temporary and will change later).
Note that there is nothing that explicitly binds together specific inputs, outputs, commands or attachments. Instead
it's up to the contract code to interpret the pieces inside the transaction and ensure they fit together correctly. This
is done to maximise flexibility for the contract developer.
Transactions may sometimes need to provide a contract with data from the outside world. Examples may include stock
prices, facts about events or the statuses of legal entities (e.g. bankruptcy), and so on. The providers of such
facts are called **oracles** and they provide facts to the ledger by signing transactions that contain commands they
recognise. The commands contain the fact and the signature shows agreement to that fact. Time is also modelled as
a fact, with the signature of a special kind of oracle called a **timestamping authority** (TSA). A TSA signs
a transaction if a pre-defined timestamping command in it defines a after/before time window that includes "true
time" (i.e. GPS time as calibrated to the US Naval Observatory).
recognise, or by creating signed attachments. The commands contain the fact and the signature shows agreement to that fact.
Time is also modelled as a fact, with the signature of a special kind of oracle called a **timestamping authority** (TSA).
A TSA signs a transaction if a pre-defined timestamping command in it defines a after/before time window that includes
"true time" (i.e. GPS time as calibrated to the US Naval Observatory). An oracle may prefer to generate a signed
attachment if the fact it's creating is relatively static and may be referred to over and over again.
As the same terminology often crops up in different distributed ledger designs, let's compare this to other
distributed ledger systems you may be familiar with. You can find more detailed design rationales for why the platform

View File

@ -28,6 +28,7 @@ Read on to learn:
data-model
messaging
running-the-trading-demo
node-administration
.. toctree::
:maxdepth: 2

View File

@ -0,0 +1,41 @@
Node administration
===================
When a node is running, it exposes an embedded web server that lets you monitor it, upload and download attachments,
access a REST API and so on.
Uploading and downloading attachments
-------------------------------------
Attachments are files that add context to and influence the behaviour of transactions. They are always identified by
hash and they are public, in that they propagate through the network to wherever they are needed.
All attachments are zip files. Thus to upload a file to the ledger you must first wrap it into a zip (or jar) file. Then
you can upload it by running this command from a UNIX terminal:
.. sourcecode:: shell
curl -F myfile=@path/to/my/file.zip http://localhost:31338/attachments/upload
The attachment will be identified by the SHA-256 hash of the contents, which you can get by doing:
.. sourcecode:: shell
shasum -a 256 file.zip
on a Mac or by using ``sha256sum`` on Linux. Alternatively, check the node logs. There is presently no way to manage
attachments from a GUI.
An attachment may be downloaded by fetching:
.. sourcecode:: shell
http://localhost:31338/attachments/DECD098666B9657314870E192CED0C3519C2C9D395507A238338F8D003929DE9
where DECD... is of course replaced with the hash identifier of your own attachment. Because attachments are always
containers, you can also fetch a specific file within the attachment by appending its path, like this:
.. sourcecode:: shell
http://localhost:31338/attachments/DECD098666B9657314870E192CED0C3519C2C9D395507A238338F8D003929DE9/path/within/zip.txt

View File

@ -87,6 +87,7 @@
<li class="toctree-l1"><a class="reference internal" href="data-model.html">Data model</a></li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul>

View File

@ -93,6 +93,7 @@
</li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul>
@ -173,16 +174,21 @@ the platform does not interpret itself, but which can parameterise execution of
arguments to the verify function. Each command has a list of <strong>public keys</strong> associated with it. The platform ensures
that the transaction is signed by every key listed in the commands before the contracts start to execute. Public keys
may be random/identityless for privacy, or linked to a well known legal identity via a <em>public key infrastructure</em> (PKI).</p>
<p>Note that there is nothing that explicitly binds together specific inputs, outputs or commands. Instead it&#8217;s up to the
contract code to interpret the pieces inside the transaction and ensure they fit together correctly. This is done to
maximise flexibility for the contract developer.</p>
<p>Commands are always embedded inside a transaction. Sometimes, there&#8217;s a larger piece of data that can be reused across
many different transactions. For this use case, we have <strong>attachments</strong>. Every transaction can refer to zero or more
attachments by hash. Attachments are always ZIP/JAR files, which may contain arbitrary content. Contract code can then
access the attachments by opening them as a JarInputStream (this is temporary and will change later).</p>
<p>Note that there is nothing that explicitly binds together specific inputs, outputs, commands or attachments. Instead
it&#8217;s up to the contract code to interpret the pieces inside the transaction and ensure they fit together correctly. This
is done to maximise flexibility for the contract developer.</p>
<p>Transactions may sometimes need to provide a contract with data from the outside world. Examples may include stock
prices, facts about events or the statuses of legal entities (e.g. bankruptcy), and so on. The providers of such
facts are called <strong>oracles</strong> and they provide facts to the ledger by signing transactions that contain commands they
recognise. The commands contain the fact and the signature shows agreement to that fact. Time is also modelled as
a fact, with the signature of a special kind of oracle called a <strong>timestamping authority</strong> (TSA). A TSA signs
a transaction if a pre-defined timestamping command in it defines a after/before time window that includes &#8220;true
time&#8221; (i.e. GPS time as calibrated to the US Naval Observatory).</p>
recognise, or by creating signed attachments. The commands contain the fact and the signature shows agreement to that fact.
Time is also modelled as a fact, with the signature of a special kind of oracle called a <strong>timestamping authority</strong> (TSA).
A TSA signs a transaction if a pre-defined timestamping command in it defines a after/before time window that includes
&#8220;true time&#8221; (i.e. GPS time as calibrated to the US Naval Observatory). An oracle may prefer to generate a signed
attachment if the fact it&#8217;s creating is relatively static and may be referred to over and over again.</p>
<p>As the same terminology often crops up in different distributed ledger designs, let&#8217;s compare this to other
distributed ledger systems you may be familiar with. You can find more detailed design rationales for why the platform
differs from existing systems in <a class="reference external" href="https://r3-cev.atlassian.net/wiki/">the R3 wiki</a>, but to summarise, the driving

View File

@ -87,6 +87,7 @@
<li class="toctree-l1"><a class="reference internal" href="data-model.html">Data model</a></li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul>

View File

@ -92,6 +92,7 @@
<li class="toctree-l1"><a class="reference internal" href="data-model.html">Data model</a></li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul>

View File

@ -87,6 +87,7 @@
<li class="toctree-l1"><a class="reference internal" href="data-model.html">Data model</a></li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul>
@ -183,6 +184,10 @@ prove or disprove the following hypothesis:</p>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a><ul>
<li class="toctree-l2"><a class="reference internal" href="node-administration.html#uploading-and-downloading-attachments">Uploading and downloading attachments</a></li>
</ul>
</li>
</ul>
</div>
<div class="toctree-wrapper compound" id="tutorials">

View File

@ -92,6 +92,7 @@
<li class="toctree-l1"><a class="reference internal" href="data-model.html">Data model</a></li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul>

View File

@ -93,6 +93,7 @@
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul>

249
docs/build/html/node-administration.html vendored Normal file
View File

@ -0,0 +1,249 @@
<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Node administration &mdash; R3 Prototyping latest documentation</title>
<link rel="stylesheet" href="_static/css/custom.css" type="text/css" />
<link rel="top" title="R3 Prototyping latest documentation" href="index.html"/>
<link rel="next" title="Writing a contract" href="tutorial.html"/>
<link rel="prev" title="Running the trading demo" href="running-the-trading-demo.html"/>
<script src="_static/js/modernizr.min.js"></script>
</head>
<body class="wy-body-for-nav" role="document">
<div class="wy-grid-for-nav">
<nav data-toggle="wy-nav-shift" class="wy-nav-side">
<div class="wy-side-scroll">
<div class="wy-side-nav-search">
<a href="index.html" class="icon icon-home"> R3 Prototyping
</a>
<div class="version">
latest
</div>
<div role="search">
<form id="rtd-search-form" class="wy-form" action="search.html" method="get">
<input type="text" name="q" placeholder="Search docs" />
<input type="hidden" name="check_keywords" value="yes" />
<input type="hidden" name="area" value="default" />
</form>
</div>
</div>
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
<p class="caption"><span class="caption-text">Overview</span></p>
<ul class="current">
<li class="toctree-l1"><a class="reference internal" href="inthebox.html">What&#8217;s included?</a></li>
<li class="toctree-l1"><a class="reference internal" href="getting-set-up.html">Getting set up</a></li>
<li class="toctree-l1"><a class="reference internal" href="data-model.html">Data model</a></li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1 current"><a class="current reference internal" href="">Node administration</a><ul>
<li class="toctree-l2"><a class="reference internal" href="#uploading-and-downloading-attachments">Uploading and downloading attachments</a></li>
</ul>
</li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Writing a contract</a></li>
<li class="toctree-l1"><a class="reference internal" href="protocol-state-machines.html">Protocol state machines</a></li>
</ul>
<p class="caption"><span class="caption-text">Appendix</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
<li class="toctree-l1"><a class="reference internal" href="codestyle.html">Code style guide</a></li>
</ul>
</div>
</div>
</nav>
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
<a href="index.html">R3 Prototyping</a>
</nav>
<div class="wy-nav-content">
<div class="rst-content">
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="index.html">Docs</a> &raquo;</li>
<li>Node administration</li>
<li class="wy-breadcrumbs-aside">
<a href="_sources/node-administration.txt" rel="nofollow"> View page source</a>
</li>
</ul>
<hr/>
</div>
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
<div itemprop="articleBody">
<div class="section" id="node-administration">
<h1>Node administration<a class="headerlink" href="#node-administration" title="Permalink to this headline"></a></h1>
<p>When a node is running, it exposes an embedded web server that lets you monitor it, upload and download attachments,
access a REST API and so on.</p>
<div class="section" id="uploading-and-downloading-attachments">
<h2>Uploading and downloading attachments<a class="headerlink" href="#uploading-and-downloading-attachments" title="Permalink to this headline"></a></h2>
<p>Attachments are files that add context to and influence the behaviour of transactions. They are always identified by
hash and they are public, in that they propagate through the network to wherever they are needed.</p>
<p>All attachments are zip files. Thus to upload a file to the ledger you must first wrap it into a zip (or jar) file. Then
you can upload it by running this command from a UNIX terminal:</p>
<div class="highlight-shell"><div class="highlight"><pre>curl -F <span class="nv">myfile</span><span class="o">=</span>@path/to/my/file.zip http://localhost:31338/attachments/upload
</pre></div>
</div>
<p>The attachment will be identified by the SHA-256 hash of the contents, which you can get by doing:</p>
<div class="highlight-shell"><div class="highlight"><pre>shasum -a <span class="m">256</span> file.zip
</pre></div>
</div>
<p>on a Mac or by using <code class="docutils literal"><span class="pre">sha256sum</span></code> on Linux. Alternatively, check the node logs. There is presently no way to manage
attachments from a GUI.</p>
<p>An attachment may be downloaded by fetching:</p>
<div class="highlight-shell"><div class="highlight"><pre>http://localhost:31338/attachments/DECD098666B9657314870E192CED0C3519C2C9D395507A238338F8D003929DE9
</pre></div>
</div>
<p>where DECD... is of course replaced with the hash identifier of your own attachment. Because attachments are always
containers, you can also fetch a specific file within the attachment by appending its path, like this:</p>
<div class="highlight-shell"><div class="highlight"><pre>http://localhost:31338/attachments/DECD098666B9657314870E192CED0C3519C2C9D395507A238338F8D003929DE9/path/within/zip.txt
</pre></div>
</div>
</div>
</div>
</div>
</div>
<footer>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="tutorial.html" class="btn btn-neutral float-right" title="Writing a contract" accesskey="n">Next <span class="fa fa-arrow-circle-right"></span></a>
<a href="running-the-trading-demo.html" class="btn btn-neutral" title="Running the trading demo" accesskey="p"><span class="fa fa-arrow-circle-left"></span> Previous</a>
</div>
<hr/>
<div role="contentinfo">
<p>
&copy; Copyright 2015, R3 CEV.
</p>
</div>
Built with <a href="http://sphinx-doc.org/">Sphinx</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
</div>
</div>
</section>
</div>
<script type="text/javascript">
var DOCUMENTATION_OPTIONS = {
URL_ROOT:'./',
VERSION:'latest',
COLLAPSE_INDEX:false,
FILE_SUFFIX:'.html',
HAS_SOURCE: true
};
</script>
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/underscore.js"></script>
<script type="text/javascript" src="_static/doctools.js"></script>
<script type="text/javascript" src="_static/js/theme.js"></script>
<script type="text/javascript">
jQuery(function () {
SphinxRtdTheme.StickyNav.enable();
});
</script>
</body>
</html>

View File

@ -88,6 +88,7 @@
<li class="toctree-l1"><a class="reference internal" href="data-model.html">Data model</a></li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul class="current">

View File

@ -88,6 +88,7 @@
<li class="toctree-l1"><a class="reference internal" href="data-model.html">Data model</a></li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul>

View File

@ -31,7 +31,7 @@
<link rel="top" title="R3 Prototyping latest documentation" href="index.html"/>
<link rel="next" title="Writing a contract" href="tutorial.html"/>
<link rel="next" title="Node administration" href="node-administration.html"/>
<link rel="prev" title="Networking and messaging" href="messaging.html"/>
@ -88,6 +88,7 @@
<li class="toctree-l1"><a class="reference internal" href="data-model.html">Data model</a></li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1 current"><a class="current reference internal" href="">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul>
@ -174,7 +175,7 @@ flags or another.</p>
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="tutorial.html" class="btn btn-neutral float-right" title="Writing a contract" accesskey="n">Next <span class="fa fa-arrow-circle-right"></span></a>
<a href="node-administration.html" class="btn btn-neutral float-right" title="Node administration" accesskey="n">Next <span class="fa fa-arrow-circle-right"></span></a>
<a href="messaging.html" class="btn btn-neutral" title="Networking and messaging" accesskey="p"><span class="fa fa-arrow-circle-left"></span> Previous</a>

View File

@ -86,6 +86,7 @@
<li class="toctree-l1"><a class="reference internal" href="data-model.html">Data model</a></li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul>

File diff suppressed because one or more lines are too long

View File

@ -32,7 +32,7 @@
<link rel="top" title="R3 Prototyping latest documentation" href="index.html"/>
<link rel="next" title="Protocol state machines" href="protocol-state-machines.html"/>
<link rel="prev" title="Running the trading demo" href="running-the-trading-demo.html"/>
<link rel="prev" title="Node administration" href="node-administration.html"/>
<script src="_static/js/modernizr.min.js"></script>
@ -88,6 +88,7 @@
<li class="toctree-l1"><a class="reference internal" href="data-model.html">Data model</a></li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul class="current">
@ -868,7 +869,7 @@ be implemented once in a separate contract, with the controlling data being held
<a href="protocol-state-machines.html" class="btn btn-neutral float-right" title="Protocol state machines" accesskey="n">Next <span class="fa fa-arrow-circle-right"></span></a>
<a href="running-the-trading-demo.html" class="btn btn-neutral" title="Running the trading demo" accesskey="p"><span class="fa fa-arrow-circle-left"></span> Previous</a>
<a href="node-administration.html" class="btn btn-neutral" title="Node administration" accesskey="p"><span class="fa fa-arrow-circle-left"></span> Previous</a>
</div>

View File

@ -88,6 +88,7 @@
<li class="toctree-l1"><a class="reference internal" href="data-model.html">Data model</a></li>
<li class="toctree-l1"><a class="reference internal" href="messaging.html">Networking and messaging</a></li>
<li class="toctree-l1"><a class="reference internal" href="running-the-trading-demo.html">Running the trading demo</a></li>
<li class="toctree-l1"><a class="reference internal" href="node-administration.html">Node administration</a></li>
</ul>
<p class="caption"><span class="caption-text">Tutorials</span></p>
<ul>

View File

@ -30,17 +30,23 @@ arguments to the verify function. Each command has a list of **public keys** ass
that the transaction is signed by every key listed in the commands before the contracts start to execute. Public keys
may be random/identityless for privacy, or linked to a well known legal identity via a *public key infrastructure* (PKI).
Note that there is nothing that explicitly binds together specific inputs, outputs or commands. Instead it's up to the
contract code to interpret the pieces inside the transaction and ensure they fit together correctly. This is done to
maximise flexibility for the contract developer.
Commands are always embedded inside a transaction. Sometimes, there's a larger piece of data that can be reused across
many different transactions. For this use case, we have **attachments**. Every transaction can refer to zero or more
attachments by hash. Attachments are always ZIP/JAR files, which may contain arbitrary content. Contract code can then
access the attachments by opening them as a JarInputStream (this is temporary and will change later).
Note that there is nothing that explicitly binds together specific inputs, outputs, commands or attachments. Instead
it's up to the contract code to interpret the pieces inside the transaction and ensure they fit together correctly. This
is done to maximise flexibility for the contract developer.
Transactions may sometimes need to provide a contract with data from the outside world. Examples may include stock
prices, facts about events or the statuses of legal entities (e.g. bankruptcy), and so on. The providers of such
facts are called **oracles** and they provide facts to the ledger by signing transactions that contain commands they
recognise. The commands contain the fact and the signature shows agreement to that fact. Time is also modelled as
a fact, with the signature of a special kind of oracle called a **timestamping authority** (TSA). A TSA signs
a transaction if a pre-defined timestamping command in it defines a after/before time window that includes "true
time" (i.e. GPS time as calibrated to the US Naval Observatory).
recognise, or by creating signed attachments. The commands contain the fact and the signature shows agreement to that fact.
Time is also modelled as a fact, with the signature of a special kind of oracle called a **timestamping authority** (TSA).
A TSA signs a transaction if a pre-defined timestamping command in it defines a after/before time window that includes
"true time" (i.e. GPS time as calibrated to the US Naval Observatory). An oracle may prefer to generate a signed
attachment if the fact it's creating is relatively static and may be referred to over and over again.
As the same terminology often crops up in different distributed ledger designs, let's compare this to other
distributed ledger systems you may be familiar with. You can find more detailed design rationales for why the platform

View File

@ -28,6 +28,7 @@ Read on to learn:
data-model
messaging
running-the-trading-demo
node-administration
.. toctree::
:maxdepth: 2

View File

@ -0,0 +1,41 @@
Node administration
===================
When a node is running, it exposes an embedded web server that lets you monitor it, upload and download attachments,
access a REST API and so on.
Uploading and downloading attachments
-------------------------------------
Attachments are files that add context to and influence the behaviour of transactions. They are always identified by
hash and they are public, in that they propagate through the network to wherever they are needed.
All attachments are zip files. Thus to upload a file to the ledger you must first wrap it into a zip (or jar) file. Then
you can upload it by running this command from a UNIX terminal:
.. sourcecode:: shell
curl -F myfile=@path/to/my/file.zip http://localhost:31338/attachments/upload
The attachment will be identified by the SHA-256 hash of the contents, which you can get by doing:
.. sourcecode:: shell
shasum -a 256 file.zip
on a Mac or by using ``sha256sum`` on Linux. Alternatively, check the node logs. There is presently no way to manage
attachments from a GUI.
An attachment may be downloaded by fetching:
.. sourcecode:: shell
http://localhost:31338/attachments/DECD098666B9657314870E192CED0C3519C2C9D395507A238338F8D003929DE9
where DECD... is of course replaced with the hash identifier of your own attachment. Because attachments are always
containers, you can also fetch a specific file within the attachment by appending its path, like this:
.. sourcecode:: shell
http://localhost:31338/attachments/DECD098666B9657314870E192CED0C3519C2C9D395507A238338F8D003929DE9/path/within/zip.txt

View File

@ -21,7 +21,7 @@ if [[ "$mode" == "buyer" ]]; then
elif [[ "$mode" == "seller" ]]; then
if [ ! -d seller ]; then
mkdir seller
echo "myLegalName = Bank of Giza" >seller/config
echo "myLegalName = Bank of London" >seller/config
fi
build/install/r3prototyping/bin/r3prototyping --dir=seller --fake-trade-with=localhost --network-address=localhost:31340 --timestamper-identity-file=buyer/identity-public --timestamper-address=localhost

View File

@ -9,15 +9,14 @@
package contracts.protocols
import co.paralleluniverse.fibers.Suspendable
import core.LedgerTransaction
import core.SignedTransaction
import core.TransactionGroup
import core.WireTransaction
import core.*
import core.crypto.SecureHash
import core.protocols.ProtocolLogic
import core.messaging.SingleMessageRecipient
import core.protocols.ProtocolLogic
import java.util.*
// NB: This code is unit tested by TwoPartyTradeProtocolTests
/**
* This protocol fetches each transaction identified by the given hashes from either disk or network, along with all
* their dependencies, and verifies them together using a single [TransactionGroup]. If no exception is thrown, then
@ -61,9 +60,9 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
if (stx != null) {
// Check the signatures on the stx first.
toVerify += stx!!.verifyToLedgerTransaction(serviceHub.identityService)
toVerify += stx!!.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
} else if (wtx != null) {
wtx!!.toLedgerTransaction(serviceHub.identityService)
wtx!!.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
}
// Run all the contracts and throw an exception if any of them reject.
@ -100,7 +99,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
// (2) If the identity service changes the assumed identity of one of the public keys, it's possible
// that the "tx in db is valid" invariant is violated if one of the contracts checks the identity! Should
// the db contain the identities that were resolved when the transaction was first checked, or should we
// accept this kind of change is possible?
// accept this kind of change is possible? Most likely solution is for identity data to be an attachment.
val nextRequests = LinkedHashSet<SecureHash>() // Keep things unique but ordered, for unit test stability.
nextRequests.addAll(depsToCheck)
@ -110,11 +109,18 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
val (fromDisk, downloads) = subProtocol(FetchTransactionsProtocol(nextRequests, otherSide))
nextRequests.clear()
// TODO: This could be done in parallel with other fetches for extra speed.
resolveMissingAttachments(downloads)
// Resolve any legal identities from known public keys in the signatures.
val downloadedTxns = downloads.map { it.verifyToLedgerTransaction(serviceHub.identityService) }
val downloadedTxns = downloads.map {
it.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
}
// Do the same for transactions loaded from disk (i.e. we checked them previously).
val loadedTxns = fromDisk.map { it.verifyToLedgerTransaction(serviceHub.identityService) }
val loadedTxns = fromDisk.map {
it.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
}
toVerify.addAll(downloadedTxns)
alreadyVerified.addAll(loadedTxns)
@ -131,4 +137,12 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
throw ExcessivelyLargeTransactionGraph()
}
}
@Suspendable
private fun resolveMissingAttachments(downloads: List<SignedTransaction>) {
val missingAttachments = downloads.flatMap { stx ->
stx.tx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null }
}
subProtocol(FetchAttachmentsProtocol(missingAttachments.toSet(), otherSide))
}
}

View File

@ -144,7 +144,7 @@ object TwoPartyTradeProtocol {
checkDependencies(it)
// This verifies that the transaction is contract-valid, even though it is missing signatures.
serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService))
serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments))
if (wtx.outputs.sumCashBy(myKeyPair.public) != price)
throw IllegalArgumentException("Transaction is not sending us the right amounnt of cash")

View File

@ -164,7 +164,7 @@ interface ServiceHub {
val dependencies = ltx.inputs.map {
storageService.validatedTransactions[it.txhash] ?: throw TransactionResolutionException(it.txhash)
}
val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService) }
val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService, storageService.attachments) }
TransactionGroup(setOf(ltx), ltxns.toSet()).verify(storageService.contractPrograms)
}
}

View File

@ -0,0 +1,38 @@
/*
* 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 core
import java.io.FileNotFoundException
/**
* Looks up identities and attachments from storage to generate a [LedgerTransaction].
*
* @throws FileNotFoundException if a required transaction was not found in storage.
*/
fun WireTransaction.toLedgerTransaction(identityService: IdentityService,
attachmentStorage: AttachmentStorage): LedgerTransaction {
val authenticatedArgs = commands.map {
val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) }
AuthenticatedObject(it.pubkeys, institutions, it.data)
}
val attachments = attachments.map {
attachmentStorage.openAttachment(it) ?: throw FileNotFoundException(it.toString())
}
return LedgerTransaction(inputs, attachments, outputs, authenticatedArgs, id)
}
/**
* Calls [verify] to check all required signatures are present, and then uses the passed [IdentityService] to call
* [WireTransaction.toLedgerTransaction] to look up well known identities from pubkeys.
*/
fun SignedTransaction.verifyToLedgerTransaction(identityService: IdentityService,
attachmentStorage: AttachmentStorage): LedgerTransaction {
verify()
return tx.toLedgerTransaction(identityService, attachmentStorage)
}

View File

@ -1,148 +0,0 @@
/*
* 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 core
import core.crypto.SecureHash
import core.utilities.loggerFor
import java.util.*
class TransactionResolutionException(val hash: SecureHash) : Exception()
class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
/**
* A TransactionGroup defines a directed acyclic graph of transactions that can be resolved with each other and then
* verified. Successful verification does not imply the non-existence of other conflicting transactions: simply that
* this subgraph does not contain conflicts and is accepted by the involved contracts.
*
* The inputs of the provided transactions must be resolvable either within the [transactions] set, or from the
* [nonVerifiedRoots] set. Transactions in the non-verified set are ignored other than for looking up input states.
*/
class TransactionGroup(val transactions: Set<LedgerTransaction>, val nonVerifiedRoots: Set<LedgerTransaction>) {
companion object {
val logger = loggerFor<TransactionGroup>()
}
/**
* Verifies the group and returns the set of resolved transactions.
*/
fun verify(programMap: ContractFactory): Set<TransactionForVerification> {
// Check that every input can be resolved to an output.
// Check that no output is referenced by more than one input.
// Cycles should be impossible due to the use of hashes as pointers.
check(transactions.intersect(nonVerifiedRoots).isEmpty())
val hashToTXMap: Map<SecureHash, List<LedgerTransaction>> = (transactions + nonVerifiedRoots).groupBy { it.hash }
val refToConsumingTXMap = hashMapOf<StateRef, LedgerTransaction>()
val resolved = HashSet<TransactionForVerification>(transactions.size)
for (tx in transactions) {
val inputs = ArrayList<ContractState>(tx.inputs.size)
for (ref in tx.inputs) {
val conflict = refToConsumingTXMap[ref]
if (conflict != null)
throw TransactionConflictException(ref, tx, conflict)
refToConsumingTXMap[ref] = tx
// Look up the connecting transaction.
val ltx = hashToTXMap[ref.txhash]?.single() ?: throw TransactionResolutionException(ref.txhash)
// Look up the output in that transaction by index.
inputs.add(ltx.outputs[ref.index])
}
resolved.add(TransactionForVerification(inputs, tx.outputs, tx.commands, tx.hash))
}
for (tx in resolved)
tx.verify(programMap)
logger.trace("Successfully run the contracts for ${resolved.size} transaction(s)")
return resolved
}
}
/** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */
data class TransactionForVerification(val inStates: List<ContractState>,
val outStates: List<ContractState>,
val commands: List<AuthenticatedObject<CommandData>>,
val origHash: SecureHash) {
override fun hashCode() = origHash.hashCode()
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
/**
* Runs the smart contracts governing this transaction.
*
* @throws TransactionVerificationException if a contract throws an exception, the original is in the cause field
* @throws IllegalStateException if a state refers to an unknown contract.
*/
@Throws(TransactionVerificationException::class, IllegalStateException::class)
fun verify(programMap: ContractFactory) {
// For each input and output state, locate the program to run. Then execute the verification function. If any
// throws an exception, the entire transaction is invalid.
val programHashes = (inStates.map { it.programRef } + outStates.map { it.programRef }).toSet()
for (hash in programHashes) {
val program: Contract = programMap[hash]
try {
program.verify(this)
} catch(e: Throwable) {
throw TransactionVerificationException(this, program, e)
}
}
}
/**
* Utilities for contract writers to incorporate into their logic.
*/
data class InOutGroup<T : ContractState>(val inputs: List<T>, val outputs: List<T>)
// A shortcut to make IDE auto-completion more intuitive for Java users.
fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority)
// For Java users.
fun <T : ContractState> groupStates(ofType: Class<T>, selector: (T) -> Any): List<InOutGroup<T>> {
val inputs = inStates.filterIsInstance(ofType)
val outputs = outStates.filterIsInstance(ofType)
val inGroups = inputs.groupBy(selector)
val outGroups = outputs.groupBy(selector)
@Suppress("DEPRECATION")
return groupStatesInternal(inGroups, outGroups)
}
// For Kotlin users: this version has nicer syntax and avoids reflection/object creation for the lambda.
inline fun <reified T : ContractState> groupStates(selector: (T) -> Any): List<InOutGroup<T>> {
val inputs = inStates.filterIsInstance<T>()
val outputs = outStates.filterIsInstance<T>()
val inGroups = inputs.groupBy(selector)
val outGroups = outputs.groupBy(selector)
@Suppress("DEPRECATION")
return groupStatesInternal(inGroups, outGroups)
}
@Deprecated("Do not use this directly: exposed as public only due to function inlining")
fun <T : ContractState> groupStatesInternal(inGroups: Map<Any, List<T>>, outGroups: Map<Any, List<T>>): List<InOutGroup<T>> {
val result = ArrayList<InOutGroup<T>>()
for ((k, v) in inGroups.entries)
result.add(InOutGroup(v, outGroups[k] ?: emptyList()))
for ((k, v) in outGroups.entries) {
if (inGroups[k] == null)
result.add(InOutGroup(emptyList(), v))
}
return result
}
}
/** Thrown if a verification fails due to a contract rejection. */
class TransactionVerificationException(val tx: TransactionForVerification, val contract: Contract, cause: Throwable?) : Exception(cause)

View File

@ -11,6 +11,7 @@ package core.node
import com.google.common.net.HostAndPort
import core.messaging.LegallyIdentifiableNode
import core.messaging.MessagingService
import core.node.servlets.AttachmentDownloadServlet
import core.node.servlets.AttachmentUploadServlet
import core.utilities.loggerFor
import org.eclipse.jetty.server.Server
@ -58,7 +59,8 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
val server = Server(port)
val handler = ServletContextHandler()
handler.setAttribute("storage", storage)
handler.addServlet(AttachmentUploadServlet::class.java, "/attachments/upload")
handler.addServlet(AttachmentUploadServlet::class.java, "/attachments")
handler.addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
server.handler = handler
server.start()
return server

View File

@ -15,11 +15,13 @@ import com.google.common.io.CountingInputStream
import core.Attachment
import core.AttachmentStorage
import core.crypto.SecureHash
import core.extractZipFile
import core.utilities.loggerFor
import java.io.FilterInputStream
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.util.*
import java.util.jar.JarInputStream
@ -35,6 +37,13 @@ class NodeAttachmentStorage(val storePath: Path) : AttachmentStorage {
@VisibleForTesting
var checkAttachmentsOnLoad = true
/**
* If true, newly inserted attachments will be unzipped to a subdirectory of the [storePath]. This is intended for
* human browsing convenience: the attachment itself will still be the file (that is, edits to the extracted directory
* will not have any effect).
*/
@Volatile var automaticallyExtractAttachments = false
init {
require(Files.isDirectory(storePath)) { "$storePath must be a directory" }
}
@ -102,14 +111,29 @@ class NodeAttachmentStorage(val storePath: Path) : AttachmentStorage {
Files.deleteIfExists(tmp)
}
log.info("Stored new attachment $id")
if (automaticallyExtractAttachments) {
val extractTo = storePath.resolve("${id}.jar")
try {
Files.createDirectory(extractTo)
extractZipFile(finalPath, extractTo)
} catch(e: Exception) {
log.error("Failed to extract attachment jar $id, ", e)
// TODO: Delete the extractTo directory here.
}
}
return id
}
private fun checkIsAValidJAR(path: Path) {
// Just iterate over the entries with verification enabled: should be good enough to catch mistakes.
JarInputStream(Files.newInputStream(path), true).use { stream ->
var cursor = stream.nextJarEntry
while (cursor != null) cursor = stream.nextJarEntry
while (true) {
val cursor = stream.nextJarEntry ?: break
val entryPath = Paths.get(cursor.name)
// Security check to stop zips trying to escape their rightful place.
if (entryPath.isAbsolute || entryPath.normalize() != entryPath)
throw IllegalArgumentException("Path is either absolute or non-normalised: $entryPath")
}
}
}
}

View File

@ -13,7 +13,10 @@ import co.paralleluniverse.fibers.Suspendable
import core.*
import core.crypto.DigitalSignature
import core.crypto.signWithECDSA
import core.messaging.*
import core.messaging.LegallyIdentifiableNode
import core.messaging.MessageRecipients
import core.messaging.MessagingService
import core.messaging.StateMachineManager
import core.protocols.ProtocolLogic
import core.serialization.SerializedBytes
import core.serialization.deserialize

View File

@ -14,6 +14,7 @@ import contracts.CommercialPaper
import contracts.protocols.TwoPartyTradeProtocol
import core.*
import core.crypto.DigitalSignature
import core.crypto.SecureHash
import core.crypto.generateKeyPair
import core.messaging.LegallyIdentifiableNode
import core.messaging.SingleMessageRecipient
@ -31,6 +32,7 @@ import java.security.PublicKey
import java.time.Instant
import java.util.*
import kotlin.system.exitProcess
import kotlin.test.assertEquals
// TRADING DEMO
//
@ -93,7 +95,14 @@ fun main(args: Array<String>) {
val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, config, timestamperId).start() }
if (listening) {
val buyer = TraderDemoProtocolBuyer()
// For demo purposes just extract attachment jars when saved to disk, so the user can explore them.
// Buyer will fetch the attachment from the seller.
val attachmentsPath = (node.storage.attachments as NodeAttachmentStorage).let {
it.automaticallyExtractAttachments = true
it.storePath
}
val buyer = TraderDemoProtocolBuyer(attachmentsPath)
ANSIProgressRenderer.progressTracker = buyer.progressTracker
node.smm.add("demo.buyer", buyer).get() // This thread will halt forever here.
} else {
@ -101,6 +110,15 @@ fun main(args: Array<String>) {
println("Need the --fake-trade-with command line argument")
exitProcess(1)
}
// Make sure we have the transaction prospectus attachment loaded into our store.
if (node.storage.attachments.openAttachment(TraderDemoProtocolSeller.PROSPECTUS_HASH) == null) {
TraderDemoProtocolSeller::class.java.getResourceAsStream("bank-of-london-cp.jar").use {
val id = node.storage.attachments.importAttachment(it)
assertEquals(TraderDemoProtocolSeller.PROSPECTUS_HASH, id)
}
}
val peerAddr = HostAndPort.fromString(options.valuesOf(fakeTradeWithArg).single()).withDefaultPort(Node.DEFAULT_PORT)
val otherSide = ArtemisMessagingService.makeRecipient(peerAddr)
val seller = TraderDemoProtocolSeller(myNetAddr, otherSide)
@ -112,7 +130,7 @@ fun main(args: Array<String>) {
// We create a couple of ad-hoc test protocols that wrap the two party trade protocol, to give us the demo logic.
class TraderDemoProtocolBuyer() : ProtocolLogic<Unit>() {
class TraderDemoProtocolBuyer(private val attachmentsPath: Path) : ProtocolLogic<Unit>() {
companion object {
object WAITING_FOR_SELLER_TO_CONNECT : ProgressTracker.Step("Waiting for seller to connect to us")
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
@ -146,17 +164,38 @@ class TraderDemoProtocolBuyer() : ProtocolLogic<Unit>() {
logger.info("Purchase complete - we are a happy customer! Final transaction is: " +
"\n\n${Emoji.renderIfSupported(tradeTX.tx)}")
logIssuanceAttachment(tradeTX)
} catch(e: Exception) {
logger.error("Something went wrong whilst trading!", e)
}
}
}
private fun logIssuanceAttachment(tradeTX: SignedTransaction) {
// Find the original CP issuance.
val search = TransactionGraphSearch(serviceHub.storageService.validatedTransactions, listOf(tradeTX.tx))
search.query = TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java)
val cpIssuance = search.call().single()
cpIssuance.attachments.first().let {
val p = attachmentsPath.toAbsolutePath().resolve("$it.jar")
logger.info("""
The issuance of the commercial paper came with an attachment. You can find it expanded in this directory:
$p
${Emoji.renderIfSupported(cpIssuance)}""")
}
}
}
class TraderDemoProtocolSeller(val myAddress: HostAndPort,
val otherSide: SingleMessageRecipient,
override val progressTracker: ProgressTracker = TraderDemoProtocolSeller.tracker()) : ProtocolLogic<Unit>() {
companion object {
val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9")
object ANNOUNCING : ProgressTracker.Step("Announcing to the buyer node")
object SELF_ISSUING : ProgressTracker.Step("Got session ID back, issuing and timestamping some commercial paper")
object TRADING : ProgressTracker.Step("Starting the trade protocol")
@ -179,7 +218,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort,
val tsa = serviceHub.networkMapService.timestampingNodes[0]
val cpOwnerKey = serviceHub.keyManagementService.freshKey()
val commercialPaper = makeFakeCommercialPaper(cpOwnerKey.public, tsa)
val commercialPaper = selfIssueSomeCommercialPaper(cpOwnerKey.public, tsa)
progressTracker.currentStep = TRADING
@ -199,19 +238,25 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort,
}
@Suspendable
fun makeFakeCommercialPaper(ownedBy: PublicKey, tsa: LegallyIdentifiableNode): StateAndRef<CommercialPaper.State> {
fun selfIssueSomeCommercialPaper(ownedBy: PublicKey, tsa: LegallyIdentifiableNode): StateAndRef<CommercialPaper.State> {
// Make a fake company that's issued its own paper.
val keyPair = generateKeyPair()
val party = Party("MegaCorp, Inc", keyPair.public)
val party = Party("Bank of London", keyPair.public)
val issuance = run {
val tx = CommercialPaper().generateIssue(party.ref(1,2,3), 1100.DOLLARS, Instant.now() + 10.days)
// TODO: Consider moving these two steps below into generateIssue.
// Attach the prospectus.
tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!)
// Timestamp it, all CP must be timestamped.
tx.setTime(Instant.now(), tsa.identity, 30.seconds)
val tsaSig = subProtocol(TimestampingProtocol(tsa, tx.toWireTransaction().serialized))
tx.checkAndAddSignature(tsaSig)
tx.signWith(keyPair)
tx.toSignedTransaction(true)
}

View File

@ -0,0 +1,64 @@
/*
* 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 core.node.servlets
import core.StorageService
import core.crypto.SecureHash
import core.utilities.loggerFor
import java.io.FileNotFoundException
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
/**
* Allows the node administrator to either download full attachment zips, or individual files within those zips.
*
* GET /attachments/123abcdef12121 -> download the zip identified by this hash
* GET /attachments/123abcdef12121/foo.txt -> download that file specifically
*
* Files are always forced to be downloads, they may not be embedded into web pages for security reasons.
*
* TODO: See if there's a way to prevent access by JavaScript.
* TODO: Provide an endpoint that exposes attachment file listings, to make attachments browseable.
*/
class AttachmentDownloadServlet : HttpServlet() {
private val log = loggerFor<AttachmentDownloadServlet>()
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
val reqPath = req.pathInfo?.substring(1)
if (reqPath == null) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST)
return
}
try {
val hash = SecureHash.parse(reqPath.substringBefore('/'))
val storage = servletContext.getAttribute("storage") as StorageService
val attachment = storage.attachments.openAttachment(hash) ?: throw FileNotFoundException()
// Don't allow case sensitive matches inside the jar, it'd just be confusing.
val subPath = reqPath.substringAfter('/', missingDelimiterValue = "").toLowerCase()
resp.contentType = "application/octet-stream"
if (subPath == "") {
resp.addHeader("Content-Disposition", "attachment; filename=\"$hash.zip\"")
attachment.open().use { it.copyTo(resp.outputStream) }
} else {
val filename = subPath.split('/').last()
resp.addHeader("Content-Disposition", "attachment; filename=\"$filename\"")
attachment.extractFile(subPath, resp.outputStream)
}
resp.outputStream.close()
} catch(e: FileNotFoundException) {
log.warn("404 Not Found whilst trying to handle attachment download request for ${servletContext.contextPath}/$reqPath")
resp.sendError(HttpServletResponse.SC_NOT_FOUND)
return
}
}
}

Binary file not shown.

View File

@ -63,6 +63,8 @@ class CommercialPaperTestsGeneric {
@Parameterized.Parameter
lateinit var thisTest: ICommercialPaperTestTemplate
val attachments = MockStorageService().attachments
@Test
fun ok() {
trade().verify()
@ -162,7 +164,7 @@ class CommercialPaperTestsGeneric {
}
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.hash, index)) })
}
@ -176,7 +178,7 @@ class CommercialPaperTestsGeneric {
timestamp(DUMMY_TIMESTAMPER)
}
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MockIdentityService)
stx.verifyToLedgerTransaction(MockIdentityService, attachments)
}
val (alicesWalletTX, alicesWallet) = cashOutputsToWallet(
@ -193,7 +195,7 @@ class CommercialPaperTestsGeneric {
ptx.signWith(MINI_CORP_KEY)
ptx.signWith(ALICE_KEY)
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MockIdentityService)
stx.verifyToLedgerTransaction(MockIdentityService, attachments)
}
// Won't be validated.
@ -209,7 +211,7 @@ class CommercialPaperTestsGeneric {
ptx.signWith(ALICE_KEY)
ptx.signWith(MINI_CORP_KEY)
ptx.timestamp(DUMMY_TIMESTAMPER)
return ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService)
return ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
}
val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)

View File

@ -29,6 +29,8 @@ class CrowdFundTests {
pledges = ArrayList<CrowdFund.Pledge>()
)
val attachments = MockStorageService().attachments
@Test
fun `key mismatch at issue`() {
transactionGroup {
@ -99,7 +101,7 @@ class CrowdFundTests {
}
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.hash, index)) })
}
@ -114,7 +116,7 @@ class CrowdFundTests {
timestamp(DUMMY_TIMESTAMPER)
}
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MockIdentityService)
stx.verifyToLedgerTransaction(MockIdentityService, attachments)
}
// let's give Alice some funds that she can invest
@ -134,7 +136,7 @@ class CrowdFundTests {
ptx.timestamp(DUMMY_TIMESTAMPER)
val stx = ptx.toSignedTransaction()
// this verify passes - the transaction contains an output cash, necessary to verify the fund command
stx.verifyToLedgerTransaction(MockIdentityService)
stx.verifyToLedgerTransaction(MockIdentityService, attachments)
}
// Won't be validated.
@ -150,7 +152,7 @@ class CrowdFundTests {
ptx.signWith(MINI_CORP_KEY)
ptx.timestamp(DUMMY_TIMESTAMPER)
val stx = ptx.toSignedTransaction()
return stx.verifyToLedgerTransaction(MockIdentityService)
return stx.verifyToLedgerTransaction(MockIdentityService, attachments)
}
val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days)

View File

@ -152,7 +152,7 @@ class TransactionGroupTests {
}.signAll()
// Now go through the conversion -> verification path with them.
val ltxns = signedTxns.map { it.verifyToLedgerTransaction(MockIdentityService) }.toSet()
val ltxns = signedTxns.map { it.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments) }.toSet()
TransactionGroup(ltxns, emptySet()).verify(MockContractFactory)
}
}

View File

@ -22,11 +22,15 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.nio.file.Path
import java.security.KeyPair
import java.security.PublicKey
import java.util.*
import java.util.concurrent.ExecutionException
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
@ -62,7 +66,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
transactionGroupFor<ContractState> {
val (aliceNode, bobNode) = net.createTwoNodes()
(bobNode.wallet as NodeWalletService).fillWithSomeTestCash(2000.DOLLARS)
val alicesFakePaper = fillUpForSeller(false, aliceNode.legallyIdentifableAddress.identity).second
val alicesFakePaper = fillUpForSeller(false, aliceNode.legallyIdentifableAddress.identity, null).second
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
@ -104,7 +108,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
val timestamperAddr = aliceNode.legallyIdentifableAddress
(bobNode.wallet as NodeWalletService).fillWithSomeTestCash(2000.DOLLARS)
val alicesFakePaper = fillUpForSeller(false, timestamperAddr.identity).second
val alicesFakePaper = fillUpForSeller(false, timestamperAddr.identity, null).second
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
@ -218,9 +222,18 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
val timestamperAddr = aliceNode.legallyIdentifableAddress
val bobNode = makeNodeWithTracking("bob")
// Insert a prospectus type attachment into the commercial paper transaction.
val stream = ByteArrayOutputStream()
JarOutputStream(stream).use {
it.putNextEntry(ZipEntry("Prospectus.txt"))
it.write("Our commercial paper is top notch stuff".toByteArray())
it.closeEntry()
}
val attachmentID = aliceNode.storage.attachments.importAttachment(ByteArrayInputStream(stream.toByteArray()))
val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public).second
val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode.services)
val alicesFakePaper = fillUpForSeller(false, timestamperAddr.identity).second
val alicesFakePaper = fillUpForSeller(false, timestamperAddr.identity, attachmentID).second
val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
val buyerSessionID = random63BitValue()
@ -260,6 +273,13 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
RecordingMap.Get(bobsFakeCash[0].id)
)
assertEquals(expected, records)
// Bob has downloaded the attachment.
bobNode.storage.attachments.openAttachment(attachmentID)!!.openAsJAR().use {
it.nextJarEntry
val contents = it.reader().readText()
assertTrue(contents.contains("Our commercial paper is top notch stuff"))
}
}
// And from Alice's perspective ...
@ -314,7 +334,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
val bobKey = bobNode.keyManagement.freshKey()
val bobsBadCash = fillUpForBuyer(bobError, bobKey.public).second
val alicesFakePaper = fillUpForSeller(aliceError, timestamperAddr.identity).second
val alicesFakePaper = fillUpForSeller(aliceError, timestamperAddr.identity, null).second
insertFakeTransactions(bobsBadCash, bobNode.services, bobNode.storage.myLegalIdentityKey, bobKey)
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
@ -401,7 +421,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
return Pair(wallet, listOf(eb1, bc1, bc2))
}
private fun TransactionGroupDSL<ContractState>.fillUpForSeller(withError: Boolean, timestamper: Party): Pair<Wallet, List<WireTransaction>> {
private fun TransactionGroupDSL<ContractState>.fillUpForSeller(withError: Boolean, timestamper: Party, attachmentID: SecureHash?): Pair<Wallet, List<WireTransaction>> {
val ap = transaction {
output("alice's paper") {
CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), ALICE, 1200.DOLLARS, TEST_TX_TIME + 7.days)
@ -409,6 +429,8 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
if (!withError)
arg(timestamper.owningKey) { TimestampCommand(TEST_TX_TIME, 30.seconds) }
if (attachmentID != null)
attachment(attachmentID)
}
val wallet = Wallet(listOf<StateAndRef<Cash.State>>(lookup("alice's paper")))

View File

@ -63,7 +63,7 @@ class NodeWalletServiceTest {
Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), freshKey.public)
signWith(MEGA_CORP_KEY)
}.toSignedTransaction()
val myOutput = usefulTX.verifyToLedgerTransaction(MockIdentityService).outRef<Cash.State>(0)
val myOutput = usefulTX.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments).outRef<Cash.State>(0)
// A tx that spends our money.
val spendTX = TransactionBuilder().apply {

View File

@ -93,7 +93,7 @@ class TransactionSerializationTests {
tx.timestamp(DUMMY_TIMESTAMPER)
tx.signWith(TestUtils.keypair)
val stx = tx.toSignedTransaction()
val ltx = stx.verifyToLedgerTransaction(MockIdentityService)
val ltx = stx.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments)
assertEquals(tx.commands().map { it.data }, ltx.commands.map { it.value })
assertEquals(tx.inputStates(), ltx.inputs)
assertEquals(tx.outputStates(), ltx.outputs)

View File

@ -110,6 +110,7 @@ class LabeledOutput(val label: String?, val state: ContractState) {
infix fun ContractState.label(label: String) = LabeledOutput(label, this)
abstract class AbstractTransactionForTest {
protected val attachments = ArrayList<SecureHash>()
protected val outStates = ArrayList<LabeledOutput>()
protected val commands = ArrayList<Command>()
@ -119,6 +120,10 @@ abstract class AbstractTransactionForTest {
return commands.map { AuthenticatedObject(it.pubkeys, it.pubkeys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, it.data) }
}
fun attachment(attachmentID: SecureHash) {
attachments.add(attachmentID)
}
fun arg(vararg key: PublicKey, c: () -> CommandData) {
val keys = listOf(*key)
commands.add(Command(c(), keys))
@ -145,7 +150,7 @@ open class TransactionForTest : AbstractTransactionForTest() {
protected fun run(time: Instant) {
val cmds = commandsToAuthenticatedObjects()
val tx = TransactionForVerification(inStates, outStates.map { it.state }, cmds, SecureHash.randomSHA256())
val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.randomSHA256())
tx.verify(MockContractFactory)
}
@ -223,7 +228,7 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
inStates.add(label.outputRef)
}
fun toWireTransaction() = WireTransaction(inStates, outStates.map { it.state }, commands)
fun toWireTransaction() = WireTransaction(inStates, attachments, outStates.map { it.state }, commands)
}
val String.output: T get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found")
@ -257,7 +262,7 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
inner class Roots {
fun transaction(vararg outputStates: LabeledOutput) {
val outs = outputStates.map { it.state }
val wtx = WireTransaction(emptyList(), outs, emptyList())
val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList())
for ((index, state) in outputStates.withIndex()) {
val label = state.label!!
labelToRefs[label] = StateRef(wtx.id, index)
@ -294,8 +299,8 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
fun transactionGroup(body: TransactionGroupDSL<T>.() -> Unit) {}
fun toTransactionGroup() = TransactionGroup(
txns.map { it.toLedgerTransaction(MockIdentityService) }.toSet(),
rootTxns.map { it.toLedgerTransaction(MockIdentityService) }.toSet()
txns.map { it.toLedgerTransaction(MockIdentityService, MockStorageService().attachments) }.toSet(),
rootTxns.map { it.toLedgerTransaction(MockIdentityService, MockStorageService().attachments) }.toSet()
)
class Failed(val index: Int, cause: Throwable) : Exception("Transaction $index didn't verify", cause)