mirror of
https://github.com/corda/corda.git
synced 2025-04-16 07:27:17 +00:00
Merged master into dynamic-loading
This commit is contained in:
commit
be52c5f1b0
2
.idea/runConfigurations/Node__seller.xml
generated
2
.idea/runConfigurations/Node__seller.xml
generated
@ -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" />
|
||||
|
10
build.gradle
10
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.
|
||||
|
@ -67,6 +67,12 @@ apply plugin: CanonicalizerPlugin
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
61
core/src/main/kotlin/core/TransactionGraphSearch.kt
Normal file
61
core/src/main/kotlin/core/TransactionGraphSearch.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
20
docs/build/html/_sources/data-model.txt
vendored
20
docs/build/html/_sources/data-model.txt
vendored
@ -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
|
||||
|
1
docs/build/html/_sources/index.txt
vendored
1
docs/build/html/_sources/index.txt
vendored
@ -28,6 +28,7 @@ Read on to learn:
|
||||
data-model
|
||||
messaging
|
||||
running-the-trading-demo
|
||||
node-administration
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
41
docs/build/html/_sources/node-administration.txt
vendored
Normal file
41
docs/build/html/_sources/node-administration.txt
vendored
Normal 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
|
||||
|
1
docs/build/html/codestyle.html
vendored
1
docs/build/html/codestyle.html
vendored
@ -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>
|
||||
|
20
docs/build/html/data-model.html
vendored
20
docs/build/html/data-model.html
vendored
@ -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’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’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’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 “true
|
||||
time” (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
|
||||
“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.</p>
|
||||
<p>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 <a class="reference external" href="https://r3-cev.atlassian.net/wiki/">the R3 wiki</a>, but to summarise, the driving
|
||||
|
1
docs/build/html/genindex.html
vendored
1
docs/build/html/genindex.html
vendored
@ -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>
|
||||
|
1
docs/build/html/getting-set-up.html
vendored
1
docs/build/html/getting-set-up.html
vendored
@ -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>
|
||||
|
5
docs/build/html/index.html
vendored
5
docs/build/html/index.html
vendored
@ -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">
|
||||
|
1
docs/build/html/inthebox.html
vendored
1
docs/build/html/inthebox.html
vendored
@ -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>
|
||||
|
1
docs/build/html/messaging.html
vendored
1
docs/build/html/messaging.html
vendored
@ -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
249
docs/build/html/node-administration.html
vendored
Normal 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 — 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’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> »</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>
|
||||
© 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>
|
1
docs/build/html/protocol-state-machines.html
vendored
1
docs/build/html/protocol-state-machines.html
vendored
@ -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">
|
||||
|
1
docs/build/html/roadmap.html
vendored
1
docs/build/html/roadmap.html
vendored
@ -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>
|
||||
|
@ -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>
|
||||
|
1
docs/build/html/search.html
vendored
1
docs/build/html/search.html
vendored
@ -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>
|
||||
|
2
docs/build/html/searchindex.js
vendored
2
docs/build/html/searchindex.js
vendored
File diff suppressed because one or more lines are too long
5
docs/build/html/tutorial.html
vendored
5
docs/build/html/tutorial.html
vendored
@ -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>
|
||||
|
||||
|
1
docs/build/html/visualiser.html
vendored
1
docs/build/html/visualiser.html
vendored
@ -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>
|
||||
|
@ -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
|
||||
|
@ -28,6 +28,7 @@ Read on to learn:
|
||||
data-model
|
||||
messaging
|
||||
running-the-trading-demo
|
||||
node-administration
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
41
docs/source/node-administration.rst
Normal file
41
docs/source/node-administration.rst
Normal 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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
38
src/main/kotlin/core/TransactionTools.kt
Normal file
38
src/main/kotlin/core/TransactionTools.kt
Normal 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)
|
||||
}
|
@ -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)
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
BIN
src/main/resources/core/node/bank-of-london-cp.jar
Normal file
BIN
src/main/resources/core/node/bank-of-london-cp.jar
Normal file
Binary file not shown.
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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")))
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user