Introduction to bitcoin smart contract - sCrypt contract practice - P2PKH contract

Previous This article mainly introduces the functions of sCrypt Visual Studio Code plug-in, a sharp tool for sCrypt language development. Now we need to practice and experience the complete design, development, testing, deployment and calling process of the sCrypt contract.

design

The first step to build any smart contract is to complete a design from the idea. Here, we choose to contract a common transaction type (P2PKH) in the bitcoin network. There are two main reasons for taking this process as an example:

  1. P2P KH is the most important transaction type in bitcoin network at present, which is very necessary for beginners to understand;
  2. By contracting this classic transaction type, we can more intuitively understand the ability and use method of sCrypt;

What is P2P KH?

The full name of P2PKH is Pay To Public Key Hash, which is the most common transaction type in bitcoin network and is used to realize the transfer function.

Its locking script is:

OP_DUP OP_HASH160 <Public Key Hash> OP_EQUALVERIFY OP_CHECKSIG

Its unlocking script is:

<Signature> <Public Key>

Let's take this series In the first article Let's talk about the principle and implementation of the example.

P2PKH - receive

If someone wants to transfer bitcoin to me, first I need to tell him the Hashi value of my public key (that is, the commonly known bitcoin address, which is equivalent to my bank card number), and then the other party uses this value to construct a P2PKH locking script (here it is recorded as LS-1) and send the transaction to the miner, who will record the transaction on the chain after verification.

P2PKH - cost

Now, when I want to spend this bitcoin, I need to provide two information to construct the unlocking script:

  • Original public key information (the above public key hash value is calculated from it 1);
  • Transaction signature calculated by using the private key corresponding to the original public key 2 Information;

After the unlocking script is constructed, a new locking script is constructed by using the public key hash value of the payee, and finally the transaction is broadcast.

P2PKH - Authentication

When miners receive this new transaction from us, they need to verify its legitimacy, which mainly involves two steps:

  1. Connect the unlocking script with the locking script in UTXO (i.e. LS-1 above) to form a complete verification script:

    <Signature> <Public Key> OP_DUP OP_HASH160 <Public Key Hash> OP_EQUALVERIFY OP_CHECKSIG

  2. Use the virtual machine to execute the validation script to check whether the execution result is valid. In fact, there are two key checks in the verification process:

    2.1 Verify whether the public key information provided in the unlocking script can calculate the public key hash value in the locking script. If it passes, it means that the public key is indeed the receiver address of the previous transaction (equivalent to verifying that the receiver address of the previous transfer is my bank card number);

    2.2 Verify whether the signature provided in the unlocking script matches the public key information. If it passes, it means that I really master the control right of the private key corresponding to this public key (equivalent to verifying that I have the password of this bank card number);

If the legitimacy verification is passed and it is proved that I really own and can control this bitcoin, the miner will record this new spending transaction on the chain. This is the main process and principle of P2P KH type transactions.

To sum up, our goal in contract design is also very clear: to achieve a sCrypt contract that is completely equivalent to the function of P2PKH.

develop

With the design ideas and objectives, we can start. First of all, of course, we should install the sCrypt plug-in in the VS Code( As described in the previous article).

sCrypt provides a Sample project It is convenient for everyone to quickly learn the development test contract. This is a good starting point. Let's start from here. First clone the project to the local, and use the command:

git clone git@github.com:scrypt-sv/boilerplate.git

In fact, the project already contains the P2P KH contract we want, so let's look at the code directly (the file is contracts/p2pkh.scrypt):

contract DemoP2PKH {
  Ripemd160 pubKeyHash;

  constructor(Ripemd160 pubKeyHash) {
    this.pubKeyHash = pubKeyHash;
  }

  public function unlock(Sig sig, PubKey pubKey) {
      require(hash160(pubKey) == this.pubKeyHash);
      require(checkSig(sig, pubKey));
  }
}

The contract is also very simple. The subjects include:

  • An attribute variable pubKeyHash of type Ripemd160. Corresponding to <public key hash> in the previous P2PKH locking script;
  • constructor. Used to complete the initialization of attribute variables;
  • A custom public function named unlock. The parameter types are Sig and PubKey respectively, corresponding to <signature> and <public key> in the previous P2PKH unlocking script; The implementation logic also corresponds to the P2P KH verification mentioned above.

Compared with the previous verification scripts in the form of Script, I believe most friends will agree that sCrypt code is easier to learn and write. Moreover, the more complex the contract logic function, the more obvious the advantages of sCrypt can be reflected.

unit testing

Once you have the code, you need to verify whether its function implementation is correct. At this time, the conventional method is to add some unit tests. The test documents for the above contract are tests/js/p2pkh.scrypttest.js , code as follows:

const path = require('path');
const { expect } = require('chai');
const { buildContractClass, bsv } = require('scrypttest');

/**
 * an example test for contract containing signature verification
 */
const { inputIndex, inputSatoshis, tx, signTx, toHex } = require('../testHelper');

const privateKey = new bsv.PrivateKey.fromRandom('testnet')
const publicKey = privateKey.publicKey
const pkh = bsv.crypto.Hash.sha256ripemd160(publicKey.toBuffer())
const privateKey2 = new bsv.PrivateKey.fromRandom('testnet')

describe('Test sCrypt contract DemoP2PKH In Javascript', () => {
  let demo
  let sig

  before(() => {
    const DemoP2PKH = buildContractClass(path.join(__dirname, '../../contracts/p2pkh.scrypt'), tx, inputIndex, inputSatoshis)
    demo = new DemoP2PKH(toHex(pkh))
  });

  it('signature check should succeed when right private key signs', () => {
    sig = signTx(tx, privateKey, demo.getLockingScript())
    expect(demo.unlock(toHex(sig), toHex(publicKey))).to.equal(true);
    /*
     * print out parameters used in debugger, see ""../.vscode/launch.json" for an example
      console.log(toHex(pkh))
      console.log(toHex(sig))
      console.log(toHex(publicKey))
      console.log(tx.uncheckedSerialize())
    */
  });

  it('signature check should fail when wrong private key signs', () => {
    sig = signTx(tx, privateKey2, demo.getLockingScript())
    expect(demo.unlock(toHex(sig), toHex(publicKey))).to.equal(false);
  });
});

Friends who are familiar with Javascript may recognize that this is a pure JS test file based on mocha + chai framework. Let's take a closer look at this test case.

First, import the Javascript / Typescript test library of sCrypt scrypttest Function:

const { buildContractClass, bsv } = require('scrypttest');

Use the tool function buildContractClass to get the class object reflected by the contract DemoP2PKH in Javascript:

const DemoP2PKH = buildContractClass(path.join(__dirname, '../../contracts/p2pkh.scrypt'), tx, inputIndex, inputSatoshis)

Instantiate the reduction class using initialization parameters (i.e. hex format of public key hash):

demo = new DemoP2PKH(toHex(pkh))

A public method for testing contract instances that should succeed:

sig = signTx(tx, privateKey, demo.getLockingScript())
expect(demo.unlock(toHex(sig), toHex(publicKey))).to.equal(true);

Or when it should fail (the signature cannot be verified because the wrong private key is used):

sig = signTx(tx, privateKey2, demo.getLockingScript())
expect(demo.unlock(toHex(sig), toHex(publicKey))).to.equal(false);

Before running the test, we need to run npm install in the root directory of the project to ensure that the test dependencies have been successfully installed; Then right click the test file in the VS Code editor and select "Run sCrypt Test"; The running results are viewed in the "OUTPUT" view.

Debug

The above unit tests alone are not enough, because when a single test fails, we can only get the final result without more internal information to help us solve the code problems of the contract itself. At this time, you need to use the Debug function of the sCrypt plug-in.

In Vscode/launch In the JSON file, we can find the Debug configuration item for the DemoP2PKH contract:

{
    "type": "scrypt",
    "request": "launch",
    "name": "Debug P2PKH",
    "program": "${workspaceFolder}/contracts/p2pkh.scrypt",
    "constructorParams": "Ripemd160(b'2bc7163e0085b0bcd4e0efd1c537537053aa13f2')",
    "entryMethod": "unlock",
    "entryMethodParams": "Sig(b'30440220729d3935d496e5a708a6a1d4c61dcdd1bebae6f0e0b63b9b9eb1b7616cdbbc2b02203b58cdde0133a6e90d921ecee6ecafca7000a13a3e38673810b4c6badd8d952041'), PubKey(b'03613fa845ad3fe1ef4fe9bbf0b50a1cb5219dd30a0c4e3e4e46fb218313af9220')",
    "txContext": {
        "hex": "01000000015884e5db9de218238671572340b207ee85b628074e7e467096c267266baf77a40000000000ffffffff0000000000",
        "inputIndex": 0,
        "inputSatoshis": 100000
    }
}

Let's explain the key parameters:

  • program: specify the contract file for the specific implementation of the configuration;
  • constructorParams: specifies the constructor parameter list of the contract. If there are multiple, use commas to connect them; In addition, if the contract does not display a constructor, the compiler will automatically generate a default constructor, so its attributes need to be passed in as a constructor parameter list in order.
  • entryMethod: Specifies the public function name to debug;
  • entryMethodParams: Specifies the argument list of the public function to be debugged. Similarly, if there are multiple arguments, they should be connected with commas;
  • txContext: Specifies the relevant context information of the current transaction during debugging, where:
    • HEX: the HEX format representation of the transaction, which can be signed transaction or unsigned transaction;
    • inputIndex: the input sequence number corresponding to the UTXO to be spent and locked by the contract;
    • inputSatoshis: the number of bitcoins in UTXO to be spent and locked by the contract, in satoshis;

Note: the parameters in constructorParams and entryMethodParams must be of the same (sub) type as the corresponding parameters in the contract, and must be of sCrypt syntax. Otherwise, an error will be reported when debugging is started.

So how are the above key parameters obtained? Let's go back to the previous test file and find a comment that can be opened and run:

/*
 * print out parameters used in debugger, see ""../.vscode/launch.json" for an example
  console.log(toHex(pkh))
  console.log(toHex(sig))
  console.log(toHex(publicKey))
  console.log(tx.uncheckedSerialize())
*/

These outputs are exactly the parameters required for Debug configuration. Similarly, other contracts can use similar methods to obtain the required parameters.

Once configured, you can use the "F5" shortcut key to start code debugging. For specific functions and usage of the debugger, see Previous article And VS Code official documents.

Test online deployment and invocation

Before using the contract in the production environment, developers should conduct necessary tests on the Testnet to ensure that the contract code meets the expectations. For this example, you can use the command node tests/Testnet/p2pkh JS.

Preparation

When we run the file for the first time, we will see output results like this:

New privKey generated for testnet: cMtFUvwk43MwBoWs15fU15jWmQEk27yJJjEkWotmPjHHRuXU9qGq
With address: moJnB7AND5TW8suRmdHPbY6knpfE1uJ15n
You could fund the address on testnet & use the privKey to complete the test

Because there are two prerequisites for normal code operation:

  1. A private key on the test network is required;
  2. There are enough bsvs (at least 10000+ satoshis) for testing in the address corresponding to the private key;

If you already have such a private key, you can find and modify the following line of code (use the private key in WIF format to replace the blank character):

const privKey = ''

Of course, you can also directly use the private key in the above output, but you need to obtain the test currency for the address in the output (for example, in the This website Collect on).

Running results

After the above preparations, you can run the use case again. Normally, you can see the following outputs:

Contract Deployed Successfully! TxId: bc929f1dddc6652896c7c162314e2651fbcd26495bd1ccf9568219e22fea2fb8
Contract Method Called Successfully! TxId: ce2dba497065d33c1e07bf710ad94e9600c6413e053b4abec2bd8562aea3dc20

The above results show that the contract deployment and invocation have been successful. You can go to this BSV blockchain browser View the corresponding transaction details in.

Code description

stay tests/testnet/p2pkh.js The complete code can be viewed in the file:

const path = require('path')
const { exit } = require('process')

const {
  buildContractClass,
  showError,
  bsv
} = require('scrypttest')

const {
  toHex,
  createLockingTx,
  createUnlockingTx,
  signTx,
  sendTx
} = require('../testHelper')

function getUnlockingScript(method, sig, publicKey) {
  if (method === 'unlock') {
    return toHex(sig) + ' ' + toHex(publicKey)
  }
}

async function main() {
  try {
    // private key on testnet in WIF
    const privKey = 'cVWvTt4tVqCHgSchQpUHch7EHcDbfXeYZnYbuqXYxpPbXQWPtrxV'
    if (!privKey) {
      const newPrivKey = new bsv.PrivateKey.fromRandom('testnet')
      console.log('New privKey generated for testnet: ' + newPrivKey.toWIF())
      console.log('With address: ' + newPrivKey.toAddress())
      console.log('You could fund the address on testnet & use the privKey to complete the test') // for example get bsv from: https://faucet.bitcoincloud.net/
      exit(1)
    }
    const privateKey = new bsv.PrivateKey.fromWIF(privKey)
    const publicKey = privateKey.publicKey

    // Initialize contract
    const P2PKH = buildContractClass(path.join(__dirname, '../../contracts/p2pkh.scrypt'))
    const publicKeyHash = bsv.crypto.Hash.sha256ripemd160(publicKey.toBuffer())
    const p2pkh = new P2PKH(toHex(publicKeyHash))

    // deploy contract on testnet
    const amountInContract = 10000
    const deployTx = await createLockingTx(privateKey.toAddress(), amountInContract)
    const lockingScript = p2pkh.getLockingScript()
    deployTx.outputs[0].setScript(bsv.Script.fromASM(lockingScript))
    deployTx.sign(privateKey)
    const deployTxId = await sendTx(deployTx)
    console.log('Contract Deployed Successfully! TxId: ', deployTxId)

    // call contract method on testnet
    const spendAmount = amountInContract / 10
    const methodCallTx = createUnlockingTx(deployTxId, amountInContract, lockingScript, spendAmount, privateKey.toAddress())
    const sig = signTx(methodCallTx, privateKey, lockingScript, amountInContract)
    const unlockingScript = getUnlockingScript('unlock', sig, publicKey)
    methodCallTx.inputs[0].setScript(bsv.Script.fromASM(unlockingScript))
    const methodCallTxId = await sendTx(methodCallTx)
    console.log('Contract Method Called Successfully! TxId: ', methodCallTxId)

  } catch (error) {
    console.log('Failed on testnet')
    showError(error)
  }
}

main()

To facilitate your understanding, let's take a look at the specific implementation of contract deployment and invocation.

  • Contract deployment:
  1. Create a new lock transaction:

    const deployTx = await createLockingTx(privateKey.toAddress(), amountInContract)

  2. Obtain the locking script corresponding to the contract:

    const lockingScript = p2pkh.getLockingScript()

  3. Set the script corresponding to the output to the above locking script:

    deployTx.outputs[0].setScript(bsv.Script.fromASM(lockingScript))

  4. Transaction signature:

    deployTx.sign(privateKey)

  5. Send transaction to service node:

    const deployTxId = await sendTx(deployTx)

  • Contract call:
  1. Create a new unlock transaction:

    const methodCallTx = createUnlockingTx(deployTxId, amountInContract, lockingScript, spendAmount, privateKey.toAddress())

  2. Get a signature for this transaction:

    const sig = signTx(methodCallTx, privateKey, lockingScript, amountInContract)

  3. Get the unlocking script corresponding to the contract method call:

    const unlockingScript = getUnlockingScript('unlock', sig, publicKey)

  4. Set the script corresponding to the input as the above unlocking script;

    methodCallTx.inputs[0].setScript(bsv.Script.fromASM(unlockingScript))

  5. Send transaction to service node:

    const methodCallTxId = await sendTx(methodCallTx)

Note: the deployment and call implementation of different contracts will vary, but the general process is similar to this example.

Concluding remarks

At this point, the series of introduction to bitcoin smart contract is over. I hope that in this way, interested friends can learn more about and participate in the development of smart contracts, and create more possibilities with blockchain technology. Please continue to pay attention, thank you:)

appendix

  1. Public key hash calculation method: calculate the hash of the public key first SHA256 Hash value, and then calculate the RIPEMD160 Hash value to obtain a 20 byte public key hash value. ↩︎

  2. For a more detailed introduction to transaction Signature, please refer to This document. ↩︎

Tags: Programming Blockchain Smart contract

Posted by knetcozd on Tue, 31 May 2022 09:40:31 +0530