diff --git a/.idea/runConfigurations/Node__seller.xml b/.idea/runConfigurations/Node__seller.xml
index cdb947943b..8e684fb972 100644
--- a/.idea/runConfigurations/Node__seller.xml
+++ b/.idea/runConfigurations/Node__seller.xml
@@ -3,7 +3,7 @@
-
+
diff --git a/build.gradle b/build.gradle
index 646ec1c2dc..89adfe9c35 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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.
diff --git a/contracts/build.gradle b/contracts/build.gradle
index 0fb7b5696b..37df6202ee 100644
--- a/contracts/build.gradle
+++ b/contracts/build.gradle
@@ -67,6 +67,12 @@ apply plugin: CanonicalizerPlugin
repositories {
mavenCentral()
+ mavenLocal()
+ mavenCentral()
+ jcenter()
+ maven {
+ url 'http://oss.sonatype.org/content/repositories/snapshots'
+ }
}
dependencies {
diff --git a/core/build.gradle b/core/build.gradle
index a159947089..ba1809ddb5 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -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"
diff --git a/core/src/main/kotlin/core/Structures.kt b/core/src/main/kotlin/core/Structures.kt
index 73aa3d45c4..56a8a0d326 100644
--- a/core/src/main/kotlin/core/Structures.kt
+++ b/core/src/main/kotlin/core/Structures.kt
@@ -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()
+ }
}
\ No newline at end of file
diff --git a/core/src/main/kotlin/core/TransactionGraphSearch.kt b/core/src/main/kotlin/core/TransactionGraphSearch.kt
new file mode 100644
index 0000000000..e6c78cdfa9
--- /dev/null
+++ b/core/src/main/kotlin/core/TransactionGraphSearch.kt
@@ -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,
+ val startPoints: List) : Callable> {
+ class Query(
+ val withCommandOfType: Class? = null
+ )
+
+ var query: Query = Query()
+
+ override fun call(): List {
+ val q = query
+
+ val next = ArrayList()
+ next += startPoints.flatMap { it.inputs.map { it.txhash } }
+
+ val results = ArrayList()
+
+ 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
+ }
+}
diff --git a/core/src/main/kotlin/core/TransactionVerification.kt b/core/src/main/kotlin/core/TransactionVerification.kt
index af3f6e0abc..5f24af239d 100644
--- a/core/src/main/kotlin/core/TransactionVerification.kt
+++ b/core/src/main/kotlin/core/TransactionVerification.kt
@@ -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, 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, 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,
val outStates: List,
+ val attachments: List,
val commands: List>,
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,
// 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)
diff --git a/core/src/main/kotlin/core/Transactions.kt b/core/src/main/kotlin/core/Transactions.kt
index 6328e42eaf..e895b78e3c 100644
--- a/core/src/main/kotlin/core/Transactions.kt
+++ b/core/src/main/kotlin/core/Transactions.kt
@@ -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,
+ val attachments: List,
val outputs: List,
val commands: List) : NamedByHash {
@@ -72,14 +74,6 @@ data class WireTransaction(val inputs: List,
}
}
- 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): SignedTransaction {
return SignedTransaction(serialized, withSigs)
@@ -98,9 +92,10 @@ data class WireTransaction(val inputs: List,
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,
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,
/** A mutable transaction that's in the process of being built, before all signatures are present. */
class TransactionBuilder(private val inputs: MutableList = arrayListOf(),
+ private val attachments: MutableList = arrayListOf(),
private val outputs: MutableList = arrayListOf(),
private val commands: MutableList = arrayListOf()) {
@@ -254,7 +241,8 @@ class TransactionBuilder(private val inputs: MutableList = 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 = 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 = arrayListOf
fun inputStates(): List = ArrayList(inputs)
fun outputStates(): List = ArrayList(outputs)
fun commands(): List = ArrayList(commands)
+ fun attachments(): List = 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,
+ /** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */
+ val attachments: List,
/** The states that will be generated by the execution of this transaction. */
val outputs: List,
/** Arbitrary data passed to the program of each input state. */
@@ -312,7 +310,7 @@ data class LedgerTransaction(
fun 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
}
diff --git a/core/src/main/kotlin/core/Utils.kt b/core/src/main/kotlin/core/Utils.kt
index f735723451..5d140972e9 100644
--- a/core/src/main/kotlin/core/Utils.kt
+++ b/core/src/main/kotlin/core/Utils.kt
@@ -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(private val initializer: () -> T) {
v = initializer()
return v!!
}
-}
\ No newline at end of file
+}
+
+/**
+ * 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()
+ }
+ }
+}
diff --git a/core/src/main/kotlin/core/crypto/CryptoUtilities.kt b/core/src/main/kotlin/core/crypto/CryptoUtilities.kt
index e1ff7e856f..7827f279eb 100644
--- a/core/src/main/kotlin/core/crypto/CryptoUtilities.kt
+++ b/core/src/main/kotlin/core/crypto/CryptoUtilities.kt
@@ -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")
}
}
diff --git a/docs/build/html/_sources/data-model.txt b/docs/build/html/_sources/data-model.txt
index 24a73c977a..62d6e3282b 100644
--- a/docs/build/html/_sources/data-model.txt
+++ b/docs/build/html/_sources/data-model.txt
@@ -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
diff --git a/docs/build/html/_sources/index.txt b/docs/build/html/_sources/index.txt
index 66ce77d925..7d3675f556 100644
--- a/docs/build/html/_sources/index.txt
+++ b/docs/build/html/_sources/index.txt
@@ -28,6 +28,7 @@ Read on to learn:
data-model
messaging
running-the-trading-demo
+ node-administration
.. toctree::
:maxdepth: 2
diff --git a/docs/build/html/_sources/node-administration.txt b/docs/build/html/_sources/node-administration.txt
new file mode 100644
index 0000000000..7f6d13417e
--- /dev/null
+++ b/docs/build/html/_sources/node-administration.txt
@@ -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
+
diff --git a/docs/build/html/codestyle.html b/docs/build/html/codestyle.html
index 1abd344073..f5ac87dd81 100644
--- a/docs/build/html/codestyle.html
+++ b/docs/build/html/codestyle.html
@@ -87,6 +87,7 @@
@@ -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 public keys 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 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
differs from existing systems in the R3 wiki, but to summarise, the driving
diff --git a/docs/build/html/genindex.html b/docs/build/html/genindex.html
index 4ac79dca93..b8b34da994 100644
--- a/docs/build/html/genindex.html
+++ b/docs/build/html/genindex.html
@@ -87,6 +87,7 @@
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:
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:
diff --git a/docs/source/data-model.rst b/docs/source/data-model.rst
index 24a73c977a..62d6e3282b 100644
--- a/docs/source/data-model.rst
+++ b/docs/source/data-model.rst
@@ -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
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 66ce77d925..7d3675f556 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -28,6 +28,7 @@ Read on to learn:
data-model
messaging
running-the-trading-demo
+ node-administration
.. toctree::
:maxdepth: 2
diff --git a/docs/source/node-administration.rst b/docs/source/node-administration.rst
new file mode 100644
index 0000000000..7f6d13417e
--- /dev/null
+++ b/docs/source/node-administration.rst
@@ -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
+
diff --git a/scripts/trader-demo.sh b/scripts/trader-demo.sh
index 59c39b2cb6..4f13c8fe52 100755
--- a/scripts/trader-demo.sh
+++ b/scripts/trader-demo.sh
@@ -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
diff --git a/src/main/kotlin/contracts/protocols/ResolveTransactionsProtocol.kt b/src/main/kotlin/contracts/protocols/ResolveTransactionsProtocol.kt
index 72e920ed67..0b5029a23b 100644
--- a/src/main/kotlin/contracts/protocols/ResolveTransactionsProtocol.kt
+++ b/src/main/kotlin/contracts/protocols/ResolveTransactionsProtocol.kt
@@ -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,
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,
// (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() // Keep things unique but ordered, for unit test stability.
nextRequests.addAll(depsToCheck)
@@ -110,11 +109,18 @@ class ResolveTransactionsProtocol(private val txHashes: Set,
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,
throw ExcessivelyLargeTransactionGraph()
}
}
+
+ @Suspendable
+ private fun resolveMissingAttachments(downloads: List) {
+ val missingAttachments = downloads.flatMap { stx ->
+ stx.tx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null }
+ }
+ subProtocol(FetchAttachmentsProtocol(missingAttachments.toSet(), otherSide))
+ }
}
diff --git a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt
index d368e7ef95..848dbaa33a 100644
--- a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt
+++ b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt
@@ -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")
diff --git a/src/main/kotlin/core/Services.kt b/src/main/kotlin/core/Services.kt
index fbd77069b5..b64c3eef54 100644
--- a/src/main/kotlin/core/Services.kt
+++ b/src/main/kotlin/core/Services.kt
@@ -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)
}
}
diff --git a/src/main/kotlin/core/TransactionTools.kt b/src/main/kotlin/core/TransactionTools.kt
new file mode 100644
index 0000000000..a0b3d5dbe8
--- /dev/null
+++ b/src/main/kotlin/core/TransactionTools.kt
@@ -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)
+}
diff --git a/src/main/kotlin/core/TransactionVerification.kt b/src/main/kotlin/core/TransactionVerification.kt
deleted file mode 100644
index e21c179deb..0000000000
--- a/src/main/kotlin/core/TransactionVerification.kt
+++ /dev/null
@@ -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, val nonVerifiedRoots: Set) {
- companion object {
- val logger = loggerFor()
- }
-
- /**
- * Verifies the group and returns the set of resolved transactions.
- */
- fun verify(programMap: ContractFactory): Set {
- // 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> = (transactions + nonVerifiedRoots).groupBy { it.hash }
- val refToConsumingTXMap = hashMapOf()
-
- val resolved = HashSet(transactions.size)
- for (tx in transactions) {
- val inputs = ArrayList(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,
- val outStates: List,
- val commands: List>,
- 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(val inputs: List, val outputs: List)
-
- // A shortcut to make IDE auto-completion more intuitive for Java users.
- fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority)
-
- // For Java users.
- fun groupStates(ofType: Class, selector: (T) -> Any): List> {
- 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 groupStates(selector: (T) -> Any): List> {
- val inputs = inStates.filterIsInstance()
- val outputs = outStates.filterIsInstance()
-
- 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 groupStatesInternal(inGroups: Map>, outGroups: Map>): List> {
- val result = ArrayList>()
-
- 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)
\ No newline at end of file
diff --git a/src/main/kotlin/core/node/Node.kt b/src/main/kotlin/core/node/Node.kt
index 753fa970ab..55baf61c73 100644
--- a/src/main/kotlin/core/node/Node.kt
+++ b/src/main/kotlin/core/node/Node.kt
@@ -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
diff --git a/src/main/kotlin/core/node/NodeAttachmentStorage.kt b/src/main/kotlin/core/node/NodeAttachmentStorage.kt
index 182218ef5e..a141b1f626 100644
--- a/src/main/kotlin/core/node/NodeAttachmentStorage.kt
+++ b/src/main/kotlin/core/node/NodeAttachmentStorage.kt
@@ -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")
+ }
}
}
}
diff --git a/src/main/kotlin/core/node/TimestamperNodeService.kt b/src/main/kotlin/core/node/TimestamperNodeService.kt
index 6375dad3a8..35d768401f 100644
--- a/src/main/kotlin/core/node/TimestamperNodeService.kt
+++ b/src/main/kotlin/core/node/TimestamperNodeService.kt
@@ -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
diff --git a/src/main/kotlin/core/node/TraderDemo.kt b/src/main/kotlin/core/node/TraderDemo.kt
index 4236b9f41e..c2027ae3bc 100644
--- a/src/main/kotlin/core/node/TraderDemo.kt
+++ b/src/main/kotlin/core/node/TraderDemo.kt
@@ -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) {
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) {
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) {
// 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() {
+class TraderDemoProtocolBuyer(private val attachmentsPath: Path) : ProtocolLogic() {
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() {
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() {
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 {
+ fun selfIssueSomeCommercialPaper(ownedBy: PublicKey, tsa: LegallyIdentifiableNode): StateAndRef {
// 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)
}
diff --git a/src/main/kotlin/core/node/servlets/AttachmentDownloadServlet.kt b/src/main/kotlin/core/node/servlets/AttachmentDownloadServlet.kt
new file mode 100644
index 0000000000..9462019915
--- /dev/null
+++ b/src/main/kotlin/core/node/servlets/AttachmentDownloadServlet.kt
@@ -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()
+
+ 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
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/core/node/bank-of-london-cp.jar b/src/main/resources/core/node/bank-of-london-cp.jar
new file mode 100644
index 0000000000..95840a556f
Binary files /dev/null and b/src/main/resources/core/node/bank-of-london-cp.jar differ
diff --git a/src/test/kotlin/contracts/CommercialPaperTests.kt b/src/test/kotlin/contracts/CommercialPaperTests.kt
index 40b0df4fe7..9f6b3a8a2c 100644
--- a/src/test/kotlin/contracts/CommercialPaperTests.kt
+++ b/src/test/kotlin/contracts/CommercialPaperTests.kt
@@ -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>> {
- 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)
diff --git a/src/test/kotlin/contracts/CrowdFundTests.kt b/src/test/kotlin/contracts/CrowdFundTests.kt
index d8292537b6..f07440d5c7 100644
--- a/src/test/kotlin/contracts/CrowdFundTests.kt
+++ b/src/test/kotlin/contracts/CrowdFundTests.kt
@@ -29,6 +29,8 @@ class CrowdFundTests {
pledges = ArrayList()
)
+ val attachments = MockStorageService().attachments
+
@Test
fun `key mismatch at issue`() {
transactionGroup {
@@ -99,7 +101,7 @@ class CrowdFundTests {
}
fun cashOutputsToWallet(vararg states: Cash.State): Pair>> {
- 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)
diff --git a/src/test/kotlin/core/TransactionGroupTests.kt b/src/test/kotlin/core/TransactionGroupTests.kt
index a43fdb452d..81746777eb 100644
--- a/src/test/kotlin/core/TransactionGroupTests.kt
+++ b/src/test/kotlin/core/TransactionGroupTests.kt
@@ -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)
}
}
\ No newline at end of file
diff --git a/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt b/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt
index 707c83f9d6..28786c0f68 100644
--- a/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt
+++ b/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt
@@ -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 {
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.fillUpForSeller(withError: Boolean, timestamper: Party): Pair> {
+ private fun TransactionGroupDSL.fillUpForSeller(withError: Boolean, timestamper: Party, attachmentID: SecureHash?): Pair> {
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>(lookup("alice's paper")))
diff --git a/src/test/kotlin/core/node/NodeWalletServiceTest.kt b/src/test/kotlin/core/node/NodeWalletServiceTest.kt
index 5c697f21ef..5f23698431 100644
--- a/src/test/kotlin/core/node/NodeWalletServiceTest.kt
+++ b/src/test/kotlin/core/node/NodeWalletServiceTest.kt
@@ -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(0)
+ val myOutput = usefulTX.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments).outRef(0)
// A tx that spends our money.
val spendTX = TransactionBuilder().apply {
diff --git a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt
index 58179a2e8e..42eb2ac5f4 100644
--- a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt
+++ b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt
@@ -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)
diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt
index 4f8fee651f..e7167d1e1d 100644
--- a/src/test/kotlin/core/testutils/TestUtils.kt
+++ b/src/test/kotlin/core/testutils/TestUtils.kt
@@ -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()
protected val outStates = ArrayList()
protected val commands = ArrayList()
@@ -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(private val stateType: Class) {
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(private val stateType: Class) {
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(private val stateType: Class) {
fun transactionGroup(body: TransactionGroupDSL.() -> 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)