How to safely swap assets between counterparties
Any MultiChain transaction can have multiple inputs and outputs, and each one can relate to a different address on the blockchain. This enables a single transaction to perform an asset exchange between two or more parties, for example sending a dollar-denominated asset from Alice to Bob, while simultaneously sending a Euro-denominated asset from Bob to Alice. Because the exchange takes place in a single transaction, it comes with a guarantee of atomicity, meaning that all of the asset transfers take place simultaneously, or none take place at all. In the finance world, this type of transaction is termed delivery-versus-payment, or DvP for short.
MultiChain provides a number of APIs which make it easy to perform these exchanges. One party begins by using preparelockunspent(from)
and createrawexchange
to create an offer of exchange, in which they specify the asset and quantity they are offering, and the asset and quantity they request in return. This offer is represented as a partial transaction, with one input and one output, signed by the offering party. Unlike regular signatures which lock down the entire set of transaction inputs and outputs, an exchange offer uses a special signature type which allows other inputs and outputs to be added, so long as the original ones stay intact (the technical name is SIGHASH_SINGLE|SIGHASH_ANYONE_CAN_PAY
).
The partial transaction representing an offer can then be sent to a specific counterparty, or distributed for anyone to review and accept. This communication can take place on the blockchain, for example using a stream, or off-chain using any method desired (even email!) Another participant receiving the offer can review it using decoderawexchange
and then decide whether to accept it. If so, they call preparelockunspent(from)
and completerawexchange
to add their own input and output to the transaction, offering the requested asset and quantity and requesting the asset and quantity offered. The final transaction will then be balanced and can be broadcast to the network using sendrawtransaction
.
The same mechanism can also be used to create more complex exchange transactions, in which multiple assets are offered or requested, and more than two blockchain participants are involved. Instead of calling completerawexchange
, a participant can use appendrawexchange
to partially meet an offer, creating a new larger partial transaction that represents a new offer of exchange. This new transaction can then be passed on to additional parties, who can check it with decoderawexchange
and add their own inputs and outputs, and so on. A transaction can only be broadcast to the network (using sendrawtransaction
) once it is balanced, meaning that the total quantities of assets in the inputs equals the total in the outputs.
MultiChain also provides the disablerawtransaction
API to disable an offer after it has been distributed. This works so long as nobody has yet accepted the offer by extending it into a balanced transaction and broadcasting that transaction to the network. An offer is disabled by spending the assets used in one of the offer’s inputs, sending them back to their source. At this point the offer becomes useless, because one of its inputs refers to a previously spent transaction output, which the blockchain’s double-spend rules prevent from being spent again.
Finally, MultiChain allows the entire process of creating and accepting atomic exchanges to be performed with private keys that are held outside of the nodes’ wallets. Instead of using the high-level exchange APIs, this is performed with the raw transaction APIs, as described in detail in the last section below. (See also the tutorial on external key management for a general explanation of this approach to private key security.)
In this tutorial, we will focus on exchanges of two different assets between two or three counterparties. The tutorial requires two servers running MultiChain, both of which are connected to the same blockchain, with no native currency or other unusual parameters. Both servers should be running multichain-cli
for that chain in interactive mode. If you don’t yet have this set up, follow the instructions in sections 1 and 2 of the Getting Started guide and then run multichain-cli chain1
on both servers. The first server’s node should also have an address with admin
and issue
permissions – this will automatically be the case if it started the chain.
Issuing the assets
Let’s begin by creating the two assets to be used in this tutorial. On the first server, let’s find an address that can create assets:
listpermissions issue
listaddresses
An address is suitable if it appears in the output of the first command, and also in the second command together with "ismine" : true
.
Enter the address here:
Now let’s find an address belonging to the other node. On the second server:
listaddresses
Enter any address for which "ismine" : true
is shown:
Now let’s issue the assets to these two addresses. Back on the first server:
issue USD 2000 0.01
A 64-character hexadecimal transaction ID should be shown for the transaction in which the USD
asset was created.
Now let’s issue another asset to the second node’s address, after granting it the necessary permissions. Still on the first server:
grant receive,send
issue JPY 50000 1
Each of these commands should have generated a new transaction ID. Now let’s check the assets are visible. On both servers:
listassets
Both servers should show the USD
asset issued with 2000 units (dollars), each divisible into 100 sub-units (cents), and the JPY
asset with 50000 units (yen) that cannot be sub-divided. Now let’s check each server’s balance:
gettotalbalances 0
The first server should show 2000 units of the USD
asset, and the second 50000 units of JPY
.
Simple atomic exchange
Now we’ll perform a simple atomic exchange, in which 100 dollars belonging to the first node are exchanged for 10500 yen belonging to the second. On the second server, let’s create a locked transaction output containing 10500 yen:
preparelockunspentfrom '{"JPY":10500}'
Copy and paste the displayed txid
:
Copy and paste the displayed vout
:
Because it is locked, this transaction output will be protected against spending unless explicitly spent or unlocked. Now we will use it to start the exchange transaction, specifying that we want 100 dollars in exchange:
createrawexchange '{"USD":100}'
This will output a large hexadecimal blob of text that contains the raw transaction data representing the offer of exchange. Copy this blob to the clipboard, then run this command on the first server:
decoderawexchange [paste-hex-blob]
The output will show exactly what is represented by this exchange offer in the offer
and ask
sections. In addition, cancomplete
should be true
meaning that the first node has the assets required to complete the exchange.
Now create a locked transaction output containing 100 dollars, still on the first server:
preparelockunspentfrom '{"USD":100}'
Copy and paste the displayed txid
:
Copy and paste the displayed vout
:
Now we will add to the exchange transaction, in order to complete it, confirming that we accept 10500 yen in exchange:
appendrawexchange [paste-hex-blob] '{"JPY":10500}'
The output should contain an even longer hexadecimal blob of text, alongside another field complete
whose value is true
. This means that the transaction is fully signed and ready for broadcast and confirmation. (We could also have used completerawexchange
here, but we’ll get to that later on.) Copy the longer blob of hexadecimal and run:
sendrawtransaction [paste-longer-hex-blob]
The response should show a transaction ID. You can check the exchange was successful by running this command on both servers:
gettotalbalances 0
The first server should have 1900 dollars and 10500 yen. The second server should have 100 dollars and 39500 yen. The exchange of assets was performed atomically in a single transaction, which you can view on both servers using:
listwallettransactions 1
Three-way exchange with metadata
You can skip this section if it’s not of interest.
Now we’ll perform a more complex exchange between three different addresses, two belonging to the first node and one to the second. We will also add some metadata to the exchange transaction. Let’s begin by creating a new address on the first server:
getnewaddress
Enter the address here:
Let’s grant this address the necessary permissions, still on the first server:
grant send,receive
And now let’s send it some of the dollars owned by this node’s other address:
sendassetfrom USD 300
A transaction ID should be displayed, and the balance for all addresses can be seen:
getmultibalances
Now let’s create a new exchange transaction, similar to the first one. On the second server:
preparelockunspentfrom '{"JPY":10500}'
Copy and paste the displayed txid
:
Copy and paste the displayed vout
:
As before, we start building the exchange transaction, specifying that we want 100 dollars in exchange for these 10500 yen:
createrawexchange '{"USD":100}'
Copy the hexadecimal blob shown on the second server to some temporary place, then switch to the first server:
preparelockunspentfrom '{"USD":60}'
Copy and paste the displayed txid
:
Copy and paste the displayed vout
:
And now we’ll add this offer of 60 dollars to the exchange transaction, request 6300 yen in exchange (pro rata as per the original offer):
appendrawexchange [paste-hex-blob] '{"JPY":6300}'
The output should contain a longer hexadecimal blob, alongside complete : false
, meaning that the exchange is not yet balanced. Copy this longer blob to the clipboard, and let’s try sending it:
sendrawtransaction [paste-longer-hex-blob]
An error should be displayed regarding a mismatch between the transaction’s input and output quantities. So let’s see which assets are still being offered and requested in this larger transaction:
decoderawexchange [paste-longer-hex-blob]
This should show that there are still a further 4200 yen on offer, and a further 40 dollars being requested. For a more detailed breakdown:
decoderawexchange [paste-longer-hex-blob] true
The exchanges
field shows all of the individual offer
and ask
stages in the exchange transaction so far, alongside the address
involved. Now let’s complete the exchange using this node’s other address:
preparelockunspentfrom '{"USD":40}'
Copy and paste the displayed txid
:
Copy and paste the displayed vout
:
We use a different API here, to ensure that no further stages can be added, and to include some metadata (signed by the last party only):
completerawexchange [paste-longer-hex-blob] '{"JPY":4200}' 476f65732074687265652077617973
This should output the longest hexadecimal blob yet, which we can finally broadcast for confirmation:
sendrawtransaction [paste-longest-hex-blob]
Now let’s see how this transaction looks to both nodes. First, from the global perspective on both servers:
listwallettransactions 1
Note how the metadata is shown in the data
field. Then on the first server, let’s see how it looks from the perspective of each address:
listaddresstransactions 1
listaddresstransactions 1
Notice how in each case, the balance
, myaddresses
and addresses
fields change, to show the transaction from each perspective.
Disabling an exchange
You can skip this section if it’s not of interest.
This section shows how an exchange can be disabled before it has been accepted and broadcast. We begin as before, on the second server:
preparelockunspentfrom '{"JPY":10500}'
Copy and paste the displayed txid
:
Copy and paste the displayed vout
:
createrawexchange '{"USD":100}'
Copy and paste the large hexadecimal blob that appears, then examine it, still on the second server:
decoderawexchange [paste-hex-blob]
Note the field candisable : true
. This means that the second server is able to disable the exchange transaction, because it has control over the output spent by one of the exchange’s inputs. Now let’s continue as normal on the first server:
preparelockunspentfrom '{"USD":100}'
Copy and paste the displayed txid
:
Copy and paste the displayed vout
:
completerawexchange [paste-hex-blob] '{"JPY":10500}'
A larger hexadecimal blob representing the completed exchange should appear, but do not copy or send it yet. Instead, we’re going to disable the exchange before it can be broadcast. Back on the second server:
disablerawtransaction [paste-hex-blob]
A regular transaction ID should be shown, for the transaction which double-spent against the exchange, meaning that the exchange transaction can no longer be confirmed. We can verify the exchange is no longer valid by examining it on either server:
decoderawexchange [paste-hex-blob]
An error should appear, stating that one the inputs has already been spent. Now go back to the first server, copy the larger hexadecimal blob that had been output by the completerawexchange
command, and run:
sendrawtransaction [paste-larger-hex-blob]
Again, an error should appear stating that the completed exchange transaction cannot be sent, because one of its inputs was spent.
Exchanging with external private keys
You can skip this section if it’s not of interest.
Finally, we will see how to conduct a simple two-way atomic exchange between addresses whose private keys are not stored within the nodes.
Let’s begin on the second server by creating a new key pair that is not stored within the node’s wallet:
createkeypairs
Copy and paste the displayed address
:
Copy and paste the displayed privkey
:
The address should then be added to the second server node as a watch-only address (i.e. without its private key):
importaddress "" false
An empty response is expected. Now we’ll do the same on the first server:
createkeypairs
Copy and paste the displayed address
:
Copy and paste the displayed privkey
:
And add the address to the first server node as a watch-only address:
importaddress "" false
Now let’s use the first server to grant send and receive permissions to these addresses:
grant , send,receive
Now the first server can transfer some units of the USD
asset to its new watch-only address:
sendassetfrom USD 200
A transaction ID should be shown. You can verify the transfer using:
getaddressbalances 0
The response should show 200 units of USD
. Let’s go back to the second server, and do the same there for 5000 units of the JPY
asset:
sendassetfrom JPY 5000
getaddressbalances 0
Now we can begin the process of preparing the offer of 2625 yen in exchange for 50 dollars on the second server. The first step is to prepare an unspent transaction output with the offer amount (2625 yen) from the watch-only address, but we cannot use preparelockunspentfrom
because the node does not have the private key. Instead we need use the raw transaction interface:
createrawsendfrom '{"":{"JPY":2625}}' '[]' lock
The '[]'
means no metadata is added to this transaction, and the lock
means that the previous UTXOs (unspent transaction outputs) selected for use in this transaction should not be selected for any other transaction. This command should output a long hexadecimal blob which then needs to be signed using the external private key:
signrawtransaction [paste-hex-blob] '[]' '[""]'
The response should contain a complete
field with value true
, along with a larger hexadecimal blob in the hex
field. (If you prefer, this signing step could also be performed by external library code, rather than passing the private key as a parameter to MultiChain’s signrawtransaction
command. If you are using multichaind-cold
for signing, you also need to provide information about the previous outputs being spent – see cold nodes for more information.) Now broadcast the fully signed transaction:
sendrawtransaction [paste-longer-hex-blob]
Copy and paste the displayed transaction ID:
This is the equivalent of the txid
output by the preparelockunspentfrom
command. We know that the corresponding vout
output index is 0
, because we built this transaction manually using createrawsendfrom
, specifying that the first output should contain the quantity desired. (A second output will have been automatically added containing change to go back to the sending address.)
Now let’s begin preparing the offer of exchange on the second server. Again, we cannot use createrawexchange
because the node is not storing the private key. Instead, we do this:
createrawtransaction '[{"txid":"","vout":0}]' '{"":{"USD":50}}' '[]' lock
The response should contain a large hexadecimal blob, containing the unsigned offer of exchange. The previously created output containing 2625 yen is offered in exchange for 50 dollars sent back to this address. Note also the lock
keyword which locks the output containing 2625 yen, as would be performed automatically by preparelockunspentfrom
.
Now we need to sign this offer of exchange using the external private key:
signrawtransaction [paste-hex-blob] '[]' '[""]' 'SINGLE|ANYONECANPAY'
The response should contain a complete
field with value true
, along with a larger hexadecimal blob in the hex
field. This larger blob is the offer of exchange, equivalent to the output from createrawexchange
. Note the crucial importance of the signature type SINGLE|ANYONECANPAY
in creating an exchange offer. This means that the transaction can be freely added to, so long as the first input and output are unmodified.
Copy the exchange offer in the hex
to the clipboard then run this command on the first server:
decoderawexchange [paste-exchange-offer]
As with any other exchange offer, the output should show offer
and ask
sections with 2625 yen and 50 dollars respectively. We are now going to complete and accept this offer on the first server. First, we need to prepare a transaction output containing 50 dollars, working around preparelockunspentfrom
in the same way as before:
createrawsendfrom '{"":{"USD":50}}' '[]' lock
This should output a long hexadecimal blob for signing with the external private key:
signrawtransaction [paste-hex-blob] '[]' '[""]'
The response should contain a complete
field with value true
, along with a larger hexadecimal blob in the hex
field to be broadcast:
sendrawtransaction [paste-longer-hex-blob]
Copy and paste the displayed transaction ID:
As before, this is the equivalent of the txid
output by the preparelockunspentfrom
command, and will be added to the exchange offer along with an additional output specifying that 2625 yen should be sent back to the same address:
appendrawtransaction [paste-exchange-offer] '[{"txid":"","vout":0}]' '{"":{"JPY":2625}}' '[]' lock
The response should contain an even longer hexadecimal blob, containing the full exchange transaction, signed by the second server but not yet signed by the first server. The accepted offer is then signed as follows:
signrawtransaction [paste-longer-hex-blob] '[]' '[""]'
The response should contain a complete
field with value true
, along with the fully completed and signed exchange transaction in the hex
field. (Note that we did not need to pass SINGLE|ANYONECANPAY
to signrawtransaction
here because this is the last input-output pair required to complete the exchange.) The complete exchange can now be broadcast in the usual way:
sendrawtransaction [paste-completed-exchange]
The response should show a transaction ID. You can check the exchange was successful by running this command on the first server:
getaddressbalances 0
A balance of 2625 yen and 150 dollars should be returned. And this command on the second server:
getaddressbalances 0
A balance of 50 dollars and 2375 should be returned. As expected, the exchange of assets was performed atomically in a single transaction, which you can view on both servers using:
listwallettransactions 1 0 true
Where to go from here
Well done! You should now be familiar with MultiChain’s atomic exchange functionality, including 3-way exchanges, exchanges with metadata, disabling offers of exchange, and performing exchanges with external keys. Here are some more things to try out:
- Performing exchanges in which more than one asset type is offered or requested, by passing objects like
{"JPY":1050, "USD":10}
topreparelockunspent(from)
,createrawexchange
,appendrawexchange
orcompleterawexchange
. - Adding a stream item rather than raw metadata to an exchange, by passing a
{"for":stream,"key":"...","data":"..."}
object for the last parameter ofcompleterawexchange
. Thepublishers
field of the stream item will only contain this last participant. - Examine the full internal structure of a completed exchange transaction by passing its hexadecimal to
decoderawtransaction
.