-
Notifications
You must be signed in to change notification settings - Fork 1
Compiling, Deploying and Unlocking your BYTOM Smart Contract
This article will describe the basic methodology of writing your Equity contract, compiling it, deploying it on the BYTOM blockchain, then running some of its Clauses to unlock it. The article might seem big, but it's neither complex or lengthy. There's a lot of pasted code that makes it seem longer than it really is!
Also keep in mind the basic workflow for Smart Contracts, during this guide:
- Write your Smart Contract
- Compile your Smart Contract
- Deploy your Smart Contract (lock an asset with it)
- Use your Smart Contract (unlock the asset previously locked with it)
- About the author
We won't get into coding specifics in this article. You will write your contract using the Equity language, a high-level instruction language that allows developers to write their own (probably) Turing-complete Smart Contracts on the BYTOM Blockchain -- and conditionally handle assets with it.
For the sake of this guide, we will be using one of the simpler BYTOM Smart Contracts, LockPosition
. It's simple to understand, and its clause is complex enough to help us build a generalized example, and a methodology that can be applied to your own Smart Contract as well.
The code for Smart Contract LockPosition
is as the following, and is available in the official BYTOM repository -- I've changed the name of some variables for readability's sake:
contract LockPosition(expirationBlockHeight: Integer,
saver: Program,
saver_key: PublicKey) locks lockAmount of lockAsset {
clause expire(sig: Signature) {
verify above(expirationBlockHeight)
verify checkTxSig(saver_key, sig)
lock lockAmount of lockAsset with saver
}
}
As you can see, it's simple enough. You can lock an amount of an Asset with it (BTM or anything else) and it will keep it locked until a certain block height has passed, and you've provided a suitable signature (same one as the one during Deployment).
So we've got:
Parameters: expireBlockHeight
of type: Integer, saver
of type: Program and saver_key
of type: PublicKey
Clauses: expire()
>> expire()
clause parameters: sig
of type: Signature.
Everything here was pretty straightforward, I think. However, here's some thoughts behind some of our previous choices: Parameters of type: Signature CAN ONLY EXIST AS
Clause
parameters, notContract
parameters. You can probably understand why: Signatures are only exist when you're running a contract clause, because you're actually sending a transaction (an invocation transaction). Signatures during contract deployment make no sense.Instead, if you want to specify Identities, Owners, Addresses to use inside your contract, do so in your Contract parameters, by declaring variables of type
PublicKey
.
We now need to compile our code into a form that is understandable by the BYTOM Virtual Machine (BVM). Therefore, we need to transform our Smart Contract into BVM operation instructions (OP_CODES) -- the process described in the docs as Compiling the contract.
In this step, we also need to fill in (instantiate is the word BYTOM uses in its documentation) the Public Key needed for the saver_key
parameter. Makes sense, right?
When you're compiling something, it needs to be ready before deployment, and specifying WHO is the beneficiary of the contract needs to happen in this step.
You can do that either by feeding the Smart Contract code in the BYTOM API (which is active when you're running a BYTOM node, either by running your wallet or running an instance of bytomd
or bytomcli
) or by feeding it into the standalone Equity compiler tool.
I personally prefer using the API because I'm always running an instance of bytomd
while developing, and it's easy enough to feed a JSON payload to it via curl POST
, but I'll do both methods here so you can compare the results.
First of all, let's look at what we need to compile and instantiate our LockPosition
smart contract:
- An
ExpirationBlockHeight
integer. That's easy, let's say we want our assets to be able to be unlocked after height215000
-- At the time of writing this, the BYTOM Mainnet is at height:214753
. - A
saver
, a Program hash to actually release the assets towards. - A
saver_key
, a Public Key that will correspond with thesaver
Proram hash (these are 1-to-1 relations, let's not mess with them).
So we need a PublicKey + Program
pair that we own and control, if we want to have future access to our assets. Luckily, the API's create-account-receiver
function produces exactly that! Check out its reference.
So:
-
Get your account ID from
Accounts -> View Details
. You can also get these programmatically, but for the sake of convenience, let's not overcomplicate things here. -
Create a payload file that we're going to use as a prepared payload for our curl request, so we don't write everything single-line line in the command line! I'm making a crate-account-receiver.json file that contains:
{ "account_id" : "0R8D5C17G0A02" }
Of course, plug your own ID here!
-
while running a node, send the following curl POST request to the API (which is listening on
localhost:9888
:curl -X POST localhost:9888/list-pubkeys -d @/path/to/list-pubkeys-payload.json
Reply:
{
"status": "success",
"data": {
"root_xpub": "b303867c51874797dc1d3071b87b4650028326894726d3abc565331555702f557f6f18ece8ece1079a776a994d624f4b956f99b5afe241043336b2413834b842",
"pubkey_infos": [
{
"pubkey": "56dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f7",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"00000000",
"01000000"
]
},
{
"pubkey": "98e095d8fe54f7513cefe0f624eb71dd3cd16650dae31ef22aaec95aac33e683",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"00000000",
"02000000"
]
},
{
"pubkey": "821c8038ebb1b140756919834a1f6ef3b41f9f4d702a1941ffb9d47e680a6db8",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"00000000",
"03000000"
]
}
]
}
}
JSON replies in the API will gradually get larger as we move on, and
bash curl
returns them as a single line. I recommend routing the output into an output.txt file so you can let your favourite text editor beautify/indent the results. Just docurl -X POST localhost:9888/list-keys >> output.txt
to keep pushing replies in a file so you can keep track of them and organize your workflow.
Success! These are the children public-keys of our root public key, our xpub. We are the owners and controllers of all of these.
Pick any pubkey from the pubkey_infos
array that you like. I usually go with the first child-pubkey.
Be careful, your xpub
is considered to be sensitive, so don't leak it.
So let's use the first child-pubkey as the saver-key
for the compilation of our LockPosition
Smart Contract. Write the key down, along with its derivation path.
{
"pubkey": "56dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f7",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"00000000",
"01000000"
]
}
You will also need this Public Key's associated Program hash. It's a Pay To Public Witness Key Hash Program hash, and you'll have to get it through the Web interface of your BYTOM node for the time being. I'm still trying to figure out a way to painlessly get both Public Key and generated Program in one Step.
So go to Accounts->View details->Addresses
and find the n-th address, n being the order of the child Public Key that you picked -- in this example, we're using our first derivated address, so grab the Program Hash of the first Address.
In this case, the Control Program is 001495a1295d75c30c4addd38476b78d3b652016694e
for PublicKey: 56dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f7
.
Now let's create the Compilation payload, using the pubkey and program hash (above) as the initialization parameter, before actually sending the Compilation request. Let's create a compilation-payload.json file, which contains the contract code, and initializations for the 3 parameters:
expiationBlockHeight
, saver
and saver_key
.
compilation-payload.json
:
{
"contract": "contract LockPosition(expireBlockHeight: Integer, saver: Program, saver_key: PublicKey) locks lockAmount of lockAsset { clause expire(sig: Signature) { verify above(expireBlockHeight) verify checkTxSig(saver_key, sig) lock lockAmount of lockAsset with saver } }",
"args": [
{"integer": 250000 },
{"string": "001495a1295d75c30c4addd38476b78d3b652016694e"},
{"string": "56dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f7"}
]
}
Ready to compile and instantiate the Smart Contract! Send it over to the API like so:
curl -X POST localhost:9888/compile -d @/path/to/compilation-payload.json >> output.txt
You should receive a (huge) succesful JSON reply:
{
"status": "success",
"data": {
"name": "LockPosition",
"source": "contract LockPosition(expireBlockHeight: Integer, saver: Program, saver_key: PublicKey) locks lockAmount of lockAsset { clause expire(sig: Signature) { verify above(expireBlockHeight) verify checkTxSig(saver_key, sig) lock lockAmount of lockAsset with saver } }",
"program": "2056dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f716001495a1295d75c30c4addd38476b78d3b652016694e0390d0037410cd9f697b7bae7cac6900c3c251547ac100c0",
"params": [
{
"name": "expireBlockHeight",
"type": "Integer"
},
{
"name": "saver",
"type": "Program"
},
{
"name": "saver_key",
"type": "PublicKey"
}
],
"value": "lockAmount of lockAsset",
"clause_info": [
{
"name": "expire",
"params": [
{
"name": "sig",
"type": "Signature"
}
],
"blockheight": [
"expireBlockHeight"
],
"values": [
{
"name": "",
"program": "saver",
"asset": "lockAsset",
"amount": "lockAmount"
}
]
}
],
"opcodes": "0x56dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f7 0x001495a1295d75c30c4addd38476b78d3b652016694e 0x90d003 DEPTH 0xcd9f697b7bae7cac6900c3c251547ac1 FALSE CHECKPREDICATE",
"error": ""
}
}
The important parts to keep here are the program
field, and also take a look at the opcodes
field. These are the translated instructions of our contract.
What did we do? We compiled and instantiated the Smart Contract, in essence we gave our logic to the compiler and received a set of instructions that will work according to this logic.
We made an empty shell of a smart contract that, when it holds assets, it will control them like we want it to, and will unlock them according to our conditions.
Time to give something for the Smart Contract to hold onto, so it becomes useful.
In our example, assets transferred to the LockPosition
contract will remain lock (the contract will return FAIL
at any attempts to unlock it), until it receives an invocation transaction that is (a) signed by the proper address and (b) is received later than expirationBlockHeight
.
We're going to build, sign and broadcast a transaction to send a CustomAsset
to the contract.
This transaction will have a GAS
expenditure (required), an expenditure of the asset itself, and we're going to denote who's going to control the assets as the transaction's output. Guess who -- the Program Hash of the contract, obtained previously!
Let's create the build-contractLoad-tx-payload.json
first:
{
"base_transaction": null,
"actions": [
{
"account_id": "0R8D5C17G0A02",
"amount": 10000000,
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"type": "spend_account"
},
{
"account_id": "0R8D5C17G0A02",
"amount": 10000000000,
"asset_id": "bd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a823",
"type": "spend_account"
},
{
"amount": 10000000000,
"asset_id": "bd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a823",
"control_program": "2056dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f716001495a1295d75c30c4addd38476b78d3b652016694e0390d0037410cd9f697b7bae7cac6900c3c251547ac100c0",
"type": "control_program"
}
],
"ttl": 0,
"time_range": 1521625823
}
Some things to note here:
- Plug in your own account ID and custom Asset hash, also your contract program hash, obtained previously.
- All amounts have to be multiplied by
10^8
. This is how most blockchain transactions do decimals, they just represent them as integers. The actual amount, as you may imagine, is the number you see here divided by10^8
. - I'm spending 1 BTM as GAS, which is probably too much but I'd like to be safe and have my tx confirmed.
- I'm sending 100 of
CustomAsset
to the contract - The amount spent by me and the amount that ends up to the Contract has to match!
Make sure you have 1 BTM in your account, and go ahead and send the transaction over to the API:
curl -X POST localhost:9888/build-transaction -d @/path/to/build-contractLoad-tx-payload.json >> output.txt
If successful, we receive a build transaction success reply like this:
{
"status": "success",
"data": {
"raw_transaction": "0701dfd5c8d50502015f015d6291f2d399bf5a60ebbc4ad204015968600e859cca8bad3ae6c3aa9bf1399c37ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80e1eb170101160014c1b1ea8ef38e313c5817623ff9f209646c86911b01000160015e6291f2d399bf5a60ebbc4ad204015968600e859cca8bad3ae6c3aa9bf1399c37bd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a8238090dfc04a000116001495a1295d75c30c4addd38476b78d3b652016694e010003013cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80b48913011600146c18bd93f068698c5e068300a30c2fadf378ba2100013dbd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a82380c8afa0250116001468f935f9390751c86a268fbd51bcb2bec1b4aa2b000177bd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a82380c8afa02501502056dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f716001495a1295d75c30c4addd38476b78d3b652016694e0390d0037410cd9f697b7bae7cac6900c3c251547ac100c000",
"signing_instructions": [
{
"position": 0,
"witness_components": [
{
"type": "raw_tx_signature",
"quorum": 1,
"keys": [
{
"xpub": "b303867c51874797dc1d3071b87b4650028326894726d3abc565331555702f557f6f18ece8ece1079a776a994d624f4b956f99b5afe241043336b2413834b842",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"01000000",
"05000000"
]
}
],
"signatures": null
},
{
"type": "data",
"value": "2213c240bb0bfd9835fabf4ed169d0ab5d4bffe769ab7215c7b7c59d2be2f1da"
}
]
},
{
"position": 1,
"witness_components": [
{
"type": "raw_tx_signature",
"quorum": 1,
"keys": [
{
"xpub": "b303867c51874797dc1d3071b87b4650028326894726d3abc565331555702f557f6f18ece8ece1079a776a994d624f4b956f99b5afe241043336b2413834b842",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"00000000",
"01000000"
]
}
],
"signatures": null
},
{
"type": "data",
"value": "56dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f7"
}
]
}
],
"fee": 10000000,
"allow_additional_actions": false
}
}
Pay attention to the huge raw_transaction
field, as well as the whole data
field in general. It contains everything. We're going to grab all of it, and sign it.
Now we need to sign this previous transaction, through the API's sign-transaction
method.
Let's create our sign-contractLoad-tx-payload.json
, which contains 2 fields: our password
, and the whole "data"
field from the previous result, which is a Transaction
object itself -- We're literally copypasting the whole Transaction object:
{
"password": "YOUR_PASS_HERE",
"transaction": {
"raw_transaction": "0701dfd5c8d50502015f015d6291f2d399bf5a60ebbc4ad204015968600e859cca8bad3ae6c3aa9bf1399c37ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80e1eb170101160014c1b1ea8ef38e313c5817623ff9f209646c86911b01000160015e6291f2d399bf5a60ebbc4ad204015968600e859cca8bad3ae6c3aa9bf1399c37bd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a8238090dfc04a000116001495a1295d75c30c4addd38476b78d3b652016694e010003013cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80b48913011600146c18bd93f068698c5e068300a30c2fadf378ba2100013dbd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a82380c8afa0250116001468f935f9390751c86a268fbd51bcb2bec1b4aa2b000177bd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a82380c8afa02501502056dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f716001495a1295d75c30c4addd38476b78d3b652016694e0390d0037410cd9f697b7bae7cac6900c3c251547ac100c000",
"signing_instructions": [
{
"position": 0,
"witness_components": [
{
"type": "raw_tx_signature",
"quorum": 1,
"keys": [
{
"xpub": "b303867c51874797dc1d3071b87b4650028326894726d3abc565331555702f557f6f18ece8ece1079a776a994d624f4b956f99b5afe241043336b2413834b842",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"01000000",
"05000000"
]
}
],
"signatures": null
},
{
"type": "data",
"value": "2213c240bb0bfd9835fabf4ed169d0ab5d4bffe769ab7215c7b7c59d2be2f1da"
}
]
},
{
"position": 1,
"witness_components": [
{
"type": "raw_tx_signature",
"quorum": 1,
"keys": [
{
"xpub": "b303867c51874797dc1d3071b87b4650028326894726d3abc565331555702f557f6f18ece8ece1079a776a994d624f4b956f99b5afe241043336b2413834b842",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"00000000",
"01000000"
]
}
],
"signatures": null
},
{
"type": "data",
"value": "56dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f7"
}
]
}
],
"fee": 10000000,
"allow_additional_actions": false
}
}
Check it out: The transaction
field in this JSON is the same as the data
field received as a reply from build-transaction
, previously. Let's send this over to the API's sign-transaction
method:
curl -X POST localhost:9888/sign-transaction -d @/path/to/sign-contractLoad-tx-payload.json >> output.txt
And once again, we receive a (huge) successful reply:
{
"status": "success",
"data": {
"transaction": {
"raw_transaction": "0701dfd5c8d50502015f015d6291f2d399bf5a60ebbc4ad204015968600e859cca8bad3ae6c3aa9bf1399c37ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80e1eb170101160014c1b1ea8ef38e313c5817623ff9f209646c86911b630240210a430b7f6b5617586362289e951459f58cf09f0d12d155cd95c148038641a039cdb81a178ccef5c244fdc1cb96b09448d34df53b97be2a4ac65bbac4fd7d0b202213c240bb0bfd9835fabf4ed169d0ab5d4bffe769ab7215c7b7c59d2be2f1da0160015e6291f2d399bf5a60ebbc4ad204015968600e859cca8bad3ae6c3aa9bf1399c37bd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a8238090dfc04a000116001495a1295d75c30c4addd38476b78d3b652016694e630240dbe36cc174db1cb5d46b034a806ec2aac74f795a2669320bede685bdb096f75803f872c23eed7c816ed9dbdbee7fc168523d21082bf190839a40574a230186052056dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f703013cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80b48913011600146c18bd93f068698c5e068300a30c2fadf378ba2100013dbd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a82380c8afa0250116001468f935f9390751c86a268fbd51bcb2bec1b4aa2b000177bd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a82380c8afa02501502056dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f716001495a1295d75c30c4addd38476b78d3b652016694e0390d0037410cd9f697b7bae7cac6900c3c251547ac100c000",
"signing_instructions": [
{
"position": 0,
"witness_components": [
{
"type": "raw_tx_signature",
"quorum": 1,
"keys": [
{
"xpub": "b303867c51874797dc1d3071b87b4650028326894726d3abc565331555702f557f6f18ece8ece1079a776a994d624f4b956f99b5afe241043336b2413834b842",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"01000000",
"05000000"
]
}
],
"signatures": [
"210a430b7f6b5617586362289e951459f58cf09f0d12d155cd95c148038641a039cdb81a178ccef5c244fdc1cb96b09448d34df53b97be2a4ac65bbac4fd7d0b"
]
},
{
"type": "data",
"value": "2213c240bb0bfd9835fabf4ed169d0ab5d4bffe769ab7215c7b7c59d2be2f1da"
}
]
},
{
"position": 1,
"witness_components": [
{
"type": "raw_tx_signature",
"quorum": 1,
"keys": [
{
"xpub": "b303867c51874797dc1d3071b87b4650028326894726d3abc565331555702f557f6f18ece8ece1079a776a994d624f4b956f99b5afe241043336b2413834b842",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"00000000",
"01000000"
]
}
],
"signatures": [
"dbe36cc174db1cb5d46b034a806ec2aac74f795a2669320bede685bdb096f75803f872c23eed7c816ed9dbdbee7fc168523d21082bf190839a40574a23018605"
]
},
{
"type": "data",
"value": "56dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f7"
}
]
}
],
"fee": 10000000,
"allow_additional_actions": false
},
"sign_complete": true
}
}
Take note of the raw_transaction
field -- We still have one final step here, to broadcast the transaction and let the network confirm it, through the API's submit-transaction
method.
This is easily done in one line. We don't need to prepare a payload for this final small step. Get the raw_transaction_field
from the step above (in this case: 0701dfd5c8d50502015f0...
) and just pass it like so:
curl -X POST localhost:9888/submit-transaction -d '{ "raw_transaction":"0701dfd5c8d50502015f015d6291f2d399bf5a60ebbc4ad204015968600e859cca8bad3ae6c3aa9bf1399c37ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80e1eb170101160014c1b1ea8ef38e313c5817623ff9f209646c86911b630240210a430b7f6b5617586362289e951459f58cf09f0d12d155cd95c148038641a039cdb81a178ccef5c244fdc1cb96b09448d34df53b97be2a4ac65bbac4fd7d0b202213c240bb0bfd9835fabf4ed169d0ab5d4bffe769ab7215c7b7c59d2be2f1da0160015e6291f2d399bf5a60ebbc4ad204015968600e859cca8bad3ae6c3aa9bf1399c37bd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a8238090dfc04a000116001495a1295d75c30c4addd38476b78d3b652016694e630240dbe36cc174db1cb5d46b034a806ec2aac74f795a2669320bede685bdb096f75803f872c23eed7c816ed9dbdbee7fc168523d21082bf190839a40574a230186052056dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f703013cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80b48913011600146c18bd93f068698c5e068300a30c2fadf378ba2100013dbd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a82380c8afa0250116001468f935f9390751c86a268fbd51bcb2bec1b4aa2b000177bd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a82380c8afa02501502056dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f716001495a1295d75c30c4addd38476b78d3b652016694e0390d0037410cd9f697b7bae7cac6900c3c251547ac100c000"}' >> output.txt
You should receive a basic success message back:
{
"status": "success",
"data": {
"tx_id": "54f999606b6fbedab0babfc85f10994110b307aef99f53221fc5c2f4379b47ee"
}
}
Our transaction is off :) Check your node, you're going to have an unconfirmed, broadcasted transaction under the Transactions menu. Give it 30 seconds and it's going to get confirmed. Our contract is now loaded and locks an actual asset!
We sent an asset from our account to the Smart Contract, so it actually has an asset to control and lock. That's the whole point. Note that the Smart Contract "lives" and is deployed on the BYTOM Blockchain at this point. It doesn't belong to either us or the end-user. We've achieved trustlessess.
Now, we'll move on to actually trying to unlock the Smart Contract by invoking one of its clauses, and see if it actually works.
The final step is to actually use the Smart Contract and let it decide, as per its internal logic -- the one we gave it -- to unlock an asset or deny unlocking.
For that, we need to spend the Smart Contract's UTXO. Strange, right? -- The UTXO doesn't belong to us. Normally, if you ever try to spend the UTXO of another user's transaction, you will fail because you'll be unable to sign the transaction succesfully (as you do not have the other user's keys). The Smart Contract UTXO, however, will allow you to spend its load UTXO if you manage to meet its internal conditions, its logic, that was set when it was created.
First of all, let's get the Smart Contract's UTXO identifiers.
To do that, we send the whole Transaction (the txid) to the list-unspent-outputs
function in the API, and it will return the Unspent Outputs of that Transaction.
Create a simple get-contract-UTXO-payload.json
:
{
"id": "49b2f4bfe20e39694f5bfc44f6005561625008b240fad24de2022527feef6086",
"smart_contract": true
}
We use the Output ID in the Transaction that we sent earlier to the Smart Contract, the one that loaded it.
To find it, go to the Transaction that you used to load up the Contract, and check out its Outputs. Find the Output (probably of type: control
) that points to the Contract hash address we used previously, and get the Output ID.
To do that programmatically, you can use the API's
get-transaction
function. Pass it the transaction ID we got at the end of Section 3, and it'll give you back the transaction's complete data, including a list of Outputs.
Don't forget, we also need the "smart_contract": true identifier too, otherwise we'll get a result that misses important data.
Now go ahead and send it to the list-unspent-outputs
method of the BYTOM API:
curl -X POST localhost:9888/list-unspent-outputs -d @/path/to/get-contract-UTXO-payload.json >> output.txt
We receive the following reply:
{
"status": "success",
"data": [
{
"account_alias": "",
"id": "49b2f4bfe20e39694f5bfc44f6005561625008b240fad24de2022527feef6086",
"asset_id": "bd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a823",
"asset_alias": "CUSTOMASSET",
"amount": 10000000000,
"account_id": "",
"address": "",
"control_program_index": 0,
"program": "2056dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f716001495a1295d75c30c4addd38476b78d3b652016694e0390d0037410cd9f697b7bae7cac6900c3c251547ac100c0",
"source_id": "b76c5ce9f98bc893135087ddb6ccd99f932f44244635021d8849072723b5572f",
"source_pos": 2,
"valid_height": 0,
"change": false,
"derive_rule": 0
}
]
}
By the way, let's talk a little about the program
field of the above reply:
BYTOM, along with any self-respecting blockchain, is supposed to be transparent.
Indeed, the program hash above is the compiled result of our code. We received it here, we also got it when we first compiled the contract.
You can send it around to anyone, and when they pass it to the API's decode-program
, they themselves can look at the source code of the contract, before actually using it.
This is how transparency is achieved. Both parties at both ends of a Smart Contract can read to the immutable source code and inner workings of the contract, before actually using it.
Anyway, moving on to actually unlocking the Smart Contract: We essentially have to spend the contract's UTXO, and control the assets. We will build a transaction that with 3 actions:
- A
spend_account_unspent_output
Action that passes arguments to the contract to "negotiate" with it and try to unlock the assets held within it.- The arguments depend on the Clause we're invoking. Our clause for the contract of this example requires a single
Signature
as a parameter. - To provide that, we will send over our
xpub
and aderivation_path
, so the network can produce the matching child keys, and test them against the Smart Contract's initialized conditions.
- The arguments depend on the Clause we're invoking. Our clause for the contract of this example requires a single
- A
control
Action, which will give control of the unlocked assets (if successful) to an address of ours.- A control action takes an Adress
Program
as an argument, not the address itself, keep that in mind. - The receiving, control address doesn't necessarily have to be the same as the one signing the transaction. The signature should only be correct up to the point where it's getting verified by the Clause.
- A control action takes an Adress
- A
spend
Action, for the transaction's GAS cost.
We will then Sign and Broadcast this transaction to the network -- and if everything goes well, the Smart Contract will unlock.
Let's create a build-contractUnlock-tx-payload.json
:
{
"base_transaction": null,
"actions": [
{
"type": "spend_account_unspent_output",
"output_id": "49b2f4bfe20e39694f5bfc44f6005561625008b240fad24de2022527feef6086",
"arguments": [
{
"type": "raw_tx_signature",
"raw_data": {
"xpub": "b303867c51874797dc1d3071b87b4650028326894726d3abc565331555702f557f6f18ece8ece1079a776a994d624f4b956f99b5afe241043336b2413834b842",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"00000000",
"01000000"
]
}
}
]
},
{
"type": "control_program",
"asset_id": "bd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a823",
"amount": 10000000000,
"control_program": "001495a1295d75c30c4addd38476b78d3b652016694e"
},
{
"type": "spend_account",
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"amount": 10000000,
"account_id": "0R8D5C17G0A02"
}
],
"ttl": 0,
"time_range": 1521625823
}
Now, before actually building the transaction, let's take a step-by-step look at what fields we included in it:
- For the
spend_account_unspent_output
Action:-
output_id
is the contract load transaction's unspent UTXO, obtained earlier. -
arguments
contains only what's needed for the signature -- ourxpub
, and thederivation_path
through which the publicKey will get produced. If the transaction is broadcast succesfully, it means that we've provided the proper signature for the transaction itself, therefore for the xpub, and therefore it is valid for the contract to produce the signature. Make sure you're filling in thederivation_path
correctly.
-
- For the
control
Action:- Make sure the amount of assets that you want this address to control, after they get unlocked from the Smart Contract, is the same as the ones in the locking transaction's UTXO, the one we're using right now.
- For the
spend_account
Action:- Easy enough. Just make sure the GAS is enough. Here, we're giving it
0.1 BTM
. If the transaction reportsSTACK_UNDERFUL
failure, you're giving too much, if the transaction remains unconfirmed after signing and broadcasting it, and you can't see any other reason for it, you might be sending too little.
- Easy enough. Just make sure the GAS is enough. Here, we're giving it
That's all for now. Let's send it over to the BYTOM API's build-transaction
API:
curl -X POST localhost/build-transaction -d @/path/to/build-contractUnlock-tx-payload.json >> output.txt
:
And the reply, if everything went correctly:
{
"status": "success",
"data": {
"raw_transaction": "0701dfd5c8d50502019b01019801b76c5ce9f98bc893135087ddb6ccd99f932f44244635021d8849072723b5572fbd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a82380c8afa0250201502056dfddaf8553955a2396025c0c4c04c4a15204c1010dfc85668c47f6fef940f716001495a1295d75c30c4addd38476b78d3b652016694e0390d0037410cd9f697b7bae7cac6900c3c251547ac100c00100015f015db76c5ce9f98bc893135087ddb6ccd99f932f44244635021d8849072723b5572fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80b4891300011600146c18bd93f068698c5e068300a30c2fadf378ba21010002013dbd7ac31cb2a50619d104498a6f94f81dd917f8f76884495611c838668b14a82380c8afa0250116001495a1295d75c30c4addd38476b78d3b652016694e00013cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc09dd810011600148ffec1b9c1da15246bdeaae5ab0bdd2c47fb956900",
"signing_instructions": [
{
"position": 0,
"witness_components": [
{
"type": "raw_tx_signature",
"quorum": 1,
"keys": [
{
"xpub": "b303867c51874797dc1d3071b87b4650028326894726d3abc565331555702f557f6f18ece8ece1079a776a994d624f4b956f99b5afe241043336b2413834b842",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"00000000",
"01000000"
]
}
],
"signatures": null
}
]
},
{
"position": 1,
"witness_components": [
{
"type": "raw_tx_signature",
"quorum": 1,
"keys": [
{
"xpub": "b303867c51874797dc1d3071b87b4650028326894726d3abc565331555702f557f6f18ece8ece1079a776a994d624f4b956f99b5afe241043336b2413834b842",
"derivation_path": [
"2c000000",
"99000000",
"01000000",
"01000000",
"06000000"
]
}
],
"signatures": null
},
{
"type": "data",
"value": "8825dee0824cf2f471b4820426822aa4ade97aa4ddb470e4af6c71a1a56451e9"
}
]
}
],
"fee": 5000000,
"allow_additional_actions": false
}
}
You can continue by signing and broadcasting the above transaction just like we did in the middle-part of Section 3 -- just copypasting the whole Transaction object of the "data"
field returned here, sending it to sign-transaction
along with your password, then broadcasting the raw tx.
My name is Dimitris Sfounis, and I'm a senior of Computer Science in the Aristotle University of Thessaloniki, in Greece. I've been programming in the blockchain field for the past 3 years, starting from Bitcoin, then NEO, and now BYTOM. I also maintain a personal page and blog -- https://dsfounis.com
I run my own project on BYTOM, Bluechain -- a blockchain-based system for the efficient recording, management and optimisation of environmental resource operations.
Through my experiences with BYTOM, and now that Bluechain is starting to take its first shy steps towards the real world and its challenges, I've come to recognize BYTOM as one of the most powerful PoW frameworks for blockchain development.
I hope this guide wasn't too heavy. I understand that terminology might be convoluted or difficult at times, and BYTOM's English documentation is not as extensive as the Chinese one, but the tools it offers are unrivaled. Even if you have background in completely different blockchain environments, spend a little time in BYTOM and don't be afraid of it. Search around its code, check out its official repository, take part in the Discord, Telegram and Wechat dev groups. You'll understand how powerful it is pretty quickly :)
Wiki written and maintained by D. Sfounis && G. Vagenas, for the Bluechain project