Introduction to Bitcoin Smart Contract (4)-sCrypt Contract Practical Chapter-P2PKH Contract

The last article mainly introduced the relevant functions of the sCrypt language development tool sCrypt Visual Studio Code plug- in. Now we are going to get started and experience the complete design, development, testing, deployment, and invocation process of the sCrypt contract.

design

The first step in building 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 into sCrypt . There are two main reasons to use this process as an example:

  1. P2PKH is currently the most important type of transaction in the Bitcoin network, and it is necessary for beginners to understand;
  2. By contracting this classic transaction type, you can more intuitively understand the capabilities and usage of sCrypt;

What is P2PKH?

The full name of P2PKH is Pay To Public Key Hash, which is the most common transaction type in the 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 the example described in the first article of this series to explain its principle and implementation.

P2PKH-receive

If someone wants to transfer bitcoin to me, I first need to tell him the hash value of my public key (that is, my 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 Recorded as LS-1) and send the transaction to the miner, and the miner will record the transaction on the chain after verification.

P2PKH-cost

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

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

After constructing the unlocking script, use the public key hash value of the payee to construct a new locking script, and finally broadcast the transaction.

P2PKH-Verification

When a miner receives my new transaction, it needs to verify its legality, which mainly involves two steps:

  1. Connect the unlocking script with the locking script in UTXO (the aforementioned LS-1) 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 this verification script to check whether the execution result is valid. In fact, there are two most critical checks in the verification process:

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

    2.2. Verify that the signature provided in the unlock script is consistent with the public key information. If it passes, it means that I do have the right to control the private key corresponding to this public key (equivalent to verifying that I have the password for this bank card number);

If the legality verification is passed, it proves that I do own and can control the bitcoin, and the miner will record the new spending transaction on the chain. This is the main process and principle of P2PKH type transactions.

In summary, our goal for contract design is also very clear: to achieve a sCrypt contract that is completely equivalent to the P2PKH function.

Development

Once we have the design ideas and goals, we can do it. First of all, of course, install the sCrypt plugin in VS Code ( as described in the previous article ).

sCrypt provides a sample project for everyone to quickly learn and develop test contracts. This is a good starting point, we also start from here, first clone the project to the local, use the command:

git clone [email protected]:scrypt-sv/boilerplate.git

In fact, this project already contains the P2PKH contract we want, so 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 main body includes:

  • A type Ripemd160of attribute variables pubKeyHash. Corresponds to the previous P2PKH lock script <Public Key Hash>;
  • Constructor constructor. Used to complete the initialization of attribute variables;
  • A custom called unlockpublic function. Parameter type respectively Sigand PubKeycorresponding to the prior P2PKH unlocking script <Signature>and <Public Key>; to achieve a logic corresponding to speak in front P2PKH verification.

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

unit test

With the code, the next step is to verify whether its function is implemented correctly. At this time, the conventional method is to add some unit tests. The test file for the above contract is tests/js/p2pkh.scrypttest.jsas 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);
  });
});

Javascript familiar friends may look to identify this is based on mocha + chaipure JS file test framework. Let's take a closer look at this test case.

First import the scrypttest function of sCrypt's Javascript / Typescript test library :

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

Using the utility function buildContractClassto obtain contract DemoP2PKHclass object reflected in Javascript:

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

Use initialization parameters (that is, the hex format of the public key hash) to instantiate the contract class:

demo = new DemoP2PKH(toHex(pkh))

The public method of the test contract instance, when it 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 tests, we need to root directory of the project run npm installto ensure that tests are dependent on the successful installation; right after this test file, select "Run sCrypt Test" in the VS Code editor; run results in "OUTPUT" view View.

Debug

Only the above unit test is not enough, because when the single test fails, we can only get the final result, and there is no more internal information to help us solve the code problem of the contract itself. At this time, you need to use the Debug function of the sCrypt plug-in.

In .vscode/launch.jsoncan be found Debug configuration items for DemoP2PKH contract file:

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

Explain the key parameters:

  • program: Specify the contract file for the specific execution of the configuration;
  • constructorParams: Specify the constructor parameter list of the contract. If there are more than one, use a comma to connect; in addition, if the contract does not have a displayed constructor, the compiler will automatically generate a default constructor, so its properties need to be used as the constructor in order. The parameter list is passed in.
  • entryMethod: Specify the name of the public function to be debugged;
  • entryMethodParams: Specify the actual parameter list of the public function to be debugged, and if there are more than one, use a comma to connect;
  • txContext: Specify the relevant context information of the current transaction during debugging, where:
    • hex:The hex format of the transaction can be signed (signed transaction) or unsigned (unsigned transaction);
    • inputIndex: The input serial number corresponding to the UTXO to be spent and locked by the contract;
    • inputSatoshis: The amount of bitcoins in the UTXO to be spent and locked by the contract, in satoshis;

Note : constructorParamsand entryMethodParamsthe corresponding parameters must be consistent with the contract parameters (sub) types, and must be sCrypt syntax. Otherwise, an error will be reported to indicate parameter problems when starting debugging.

So how are the above parameters generally obtained? Looking back at the previous test file, you can find that there are several commented command line outputs:

/*
 * 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 needed for Debug configuration, and similarly, other contracts can also be used in similar ways to get what they need.

After proper configuration, you can use the "F5" shortcut to start code debugging. The specific functions and usage of the debugger can also be found in the previous article and VS Code official documentation.

Deploy and call on testnet

Before using the contract in a production environment, the developer should conduct necessary tests on the Testnet to ensure that the contract code meets expectations. For example in this article, you can use the command in the project root directory node tests/testnet/p2pkh.jsto run.

Ready to work

When we run the file for the first time, we will see output similar to 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. Need a private key on the testnet;
  2. There is enough BSV (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 instead of empty characters):

const privKey = ''

Of course, you can also directly use the private key in the output result above, but you need to obtain the test coin for the address in the output result (for example , collect it on this website ).

operation result

After doing the aforementioned preparations, you can run this use case again. Under normal circumstances, you can see the following output:

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

The above results show that the contract deployment and call have been successful, you can go to the BSV blockchain browser to view the corresponding transaction details (use the TxId in the output result to query).

Code description

In the tests/testnet/p2pkh.jsfile, you can view the complete code:

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

In order to facilitate everyone's understanding, let's take a look at the specific implementation of contract deployment and invocation.

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

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

  2. Get the lock 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 the transaction to the 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 the 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 to the above unlocking script;

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

  5. Send the transaction to the 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

Having said that, the series of Getting Started with Bitcoin Smart Contracts is also 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: first calculate the SHA256 hash value of the public key , and then calculate the RIPEMD160 hash value of the foregoing result to obtain a 20-byte public key hash value. ↩︎

  2. For a more detailed introduction of transaction signature (Signature), please refer to this document . ↩︎

Guess you like

Origin blog.csdn.net/freedomhero/article/details/107235041