Working with attachments

This tutorial outlines how to work with attachments, also known as contract attachments.

Introduction

Attachments are ZIP/JAR files referenced from transaction by hash, but not included in the transaction itself. These files are automatically requested from the node sending the transaction when needed and cached locally so they are not re-requested if encountered again. Attachments typically contain:

  • Contract code
  • Metadata about a transaction, such as PDF version of an invoice being settled
  • Shared information to be permanently recorded on the ledger

Uploading and downloading attachments

To add attachments, the file must first be uploaded to the node, which returns a unique ID that can be added using TransactionBuilder.addAttachment().

It is encouraged that, where possible, attachments are reusable data, so that nodes can meaningfully cache them.

Uploading an attachment

To upload an attachment to the node, you need to first connect to the relevant node. You can do this via the Corda RPC Client, as described in Interacting with a node or you can upload your attachment via the Node shell.

To upload an attachment, run the following command:

run uploadAttachment jar: <insert path-to-the-file>.jar

Alternatively, if you want to include the metadata with the attachment which can be used to find it later on, run the following command:

run uploadAttachmentWithMetadata jar: path/to/the/file.jar, uploader: myself, filename: original_name.jar

Note that currently both uploader and filename are just plain strings - there is no connection between uploader and the RPC users, for example).

The file is uploaded, checked and if successful the hash of the file is returned. This is how the attachment is identified inside the node.

Downloading an attachment

To download an attachment named by its hash, you need to first connect to the relevant node. You can do this via the Corda RPC Client, as described in Interacting with a node or you can upload your attachment via the Node shell.

To download an attachment, run the following command, replacing the ID with the hash of the attachment that you want to download:

run openAttachment id: AB7FED7663A3F195A59A0F01091932B15C22405CB727A1518418BF53C6E6663A

You will be prompted to provide a path to save the file to. To do the same thing programmatically, you can pass a simple InputStream or SecureHash to the uploadAttachment/openAttachment RPCs from a JVM client.

Searching for attachments

Attachment metadata can be queried in a similar way to the vault (see API: Vault Query).

AttachmentQueryCriteria can be used to build a query using the following set of column operations:

  • Binary logical (AND, OR)
  • Comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL)
  • Equality (EQUAL, NOT_EQUAL)
  • Likeness (LIKE, NOT_LIKE)
  • Nullability (IS_NULL, NOT_NULL)
  • Collection based (IN, NOT_IN)

The and and or operators can be used to build complex queries. For example:


assertEquals(
        emptyList(),
        storage.queryAttachments(
                AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexA"))
                        .and(AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexB"))))
)

assertEquals(
        listOf(hashA, hashB),
        storage.queryAttachments(
                AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexA"))
                        .or(AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexB"))))
)

val complexCondition =
        (uploaderCondition("complexB").and(filenamerCondition("archiveB.zip"))).or(filenamerCondition("archiveC.zip"))


NodeAttachmentServiceTest.kt

Fetching attachments

Normally, attachments on transactions are fetched automatically via the ReceiveTransactionFlow. Attachments are needed in order to validate a transaction (they include, for example, the contract code), so must be fetched before the validation process can run.

Example

There is a worked example of attachments, which relays a simple document from one node to another. The “two party trade flow” also includes an attachment; however, it is a significantly more complex demo, and less well suited for a tutorial.

The demo code is in the file samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt, with the core logic contained within the two functions recipient() and sender(). The first thing it does is set up an RPC connection to node B using a demo user account (this is all configured in the gradle build script for the demo and the nodes will be created using the deployNodes gradle task as normal). The CordaRPCClient.use method is a convenience helper intended for small tools that sets up an RPC connection scoped to the provided block, and brings all the RPCs into scope. Once connected the sender/recipient functions are run with the RPC proxy as a parameter.

We’ll look at the recipient function first.

The first thing it does is wait to receive a notification of a new transaction by calling the verifiedTransactions RPC, which returns both a snapshot and an observable of changes. The observable is made blocking and the next transaction the node verifies is retrieved. That transaction is checked to see if it has the expected attachment and if so, printed out.

fun recipient(rpc: CordaRPCOps, webPort: Int) {
    println("Waiting to receive transaction ...")
    val stx = rpc.internalVerifiedTransactionsFeed().updates.toBlocking().first()
    val wtx = stx.tx
    if (wtx.attachments.isNotEmpty()) {
        if (wtx.outputs.isNotEmpty()) {
            val state = wtx.outputsOfType<AttachmentContract.State>().single()
            require(rpc.attachmentExists(state.hash)) { "attachment matching hash: ${state.hash} does not exist" }

            // Download the attachment via the Web endpoint.
            val connection = URL("http://localhost:$webPort/attachments/${state.hash}").openConnection() as HttpURLConnection
            try {
                require(connection.responseCode == SC_OK) { "HTTP status code was ${connection.responseCode}" }
                require(connection.contentType == APPLICATION_OCTET_STREAM) { "Content-Type header was ${connection.contentType}" }
                require(connection.getHeaderField(CONTENT_DISPOSITION) == "attachment; filename=\"${state.hash}.zip\"") {
                    "Content-Disposition header was ${connection.getHeaderField(CONTENT_DISPOSITION)}"
                }

                // Write out the entries inside this jar.
                println("Attachment JAR contains these entries:")
                JarInputStream(connection.inputStream).use {
                    while (true) {
                        val e = it.nextJarEntry ?: break
                        println("Entry> ${e.name}")
                        it.closeEntry()
                    }
                }
            } finally {
                connection.disconnect()
            }
            println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(wtx)}")
        } else {
            println("Error: no output state found in ${wtx.id}")
        }
    } else {
        println("Error: no attachments found in ${wtx.id}")
    }
}

AttachmentDemo.kt

The sender correspondingly builds a transaction with the attachment, then calls FinalityFlow to complete the transaction and send it to the recipient node:

fun sender(rpc: CordaRPCOps, numOfClearBytes: Int = 1024) { // default size 1K.
    val (inputStream, hash) = InputStreamAndHash.createInMemoryTestZip(numOfClearBytes, 0)
    sender(rpc, inputStream, hash)
}

private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.SHA256) {
    // Get the identity key of the other side (the recipient).
    val notaryParty = rpc.partiesFromName("Notary", false).firstOrNull() ?: throw IllegalArgumentException("Couldn't find notary party")
    val bankBParty = rpc.partiesFromName("Bank B", false).firstOrNull() ?: throw IllegalArgumentException("Couldn't find Bank B party")
    // Make sure we have the file in storage
    if (!rpc.attachmentExists(hash)) {
        inputStream.use {
            val id = rpc.uploadAttachment(it)
            require(hash == id) { "Id was '$id' instead of '$hash'" }
        }
        require(rpc.attachmentExists(hash)) { "Attachment matching hash: $hash does not exist" }
    }

    val flowHandle = rpc.startTrackedFlow(::AttachmentDemoFlow, bankBParty, notaryParty, hash)
    flowHandle.progress.subscribe(::println)
    val stx = flowHandle.returnValue.getOrThrow()
    println("Sent ${stx.id}")
}

AttachmentDemo.kt

This side is a bit more complex. Firstly, it looks up its counterparty by name in the network map. Then, if the node doesn’t already have the attachment in its storage, we upload it from a JAR resource and check the hash was what we expected. Then a trivial transaction is built that has the attachment and a single signature and it’s sent to the other side using the FinalityFlow. The result of starting the flow is a stream of progress messages and a returnValue observable that can be used to watch out for the flow completing successfully.