Skip to content

Compiling, Deploying and Unlocking your BYTOM Smart Contract

Dimitris Sfounis edited this page Apr 17, 2019 · 11 revisions

Compiling, deploying and running your BYTOM Smart Contract

Introduction

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:

smart contract workflow

Table of Contents:

  1. Write your Smart Contract
  2. Compile your Smart Contract
  3. Deploy your Smart Contract (lock an asset with it)
  4. Use your Smart Contract (unlock the asset previously locked with it)
  5. About the author

Section 1: Write your Smart Contract

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, saverof type: Program and saver_keyof type: PublicKey Clauses: expire() >> expire() clause parameters: sig of type: Signature.

SECTION 1 RECAP ###

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, not Contract 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.

Section 2: Compile your Smart Contract

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.

2.1 Compiling through the BYTOM API (while running a node):

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 height 215000 -- 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 the saver 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"
        ]
      }
    ]
  }
}

NOTE:

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 do curl -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.

SECTION 2 RECAP ###

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.

Section 3: Deploy your Smart Contract (actually lock some assets)

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 by 10^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 CustomAssetto 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! transaction sent!

SECTION 3 RECAP ###

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.

Section 4: Unlock the Asset being controlled by our Smart Contract (invoking its Clauses)

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 a derivation_path, so the network can produce the matching child keys, and test them against the Smart Contract's initialized conditions.
  • 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 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 -- our xpub, and the derivation_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 the derivation_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 reports STACK_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.

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
  }
}

That's it!

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.


About the author:

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 :)

Clone this wiki locally