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:
- P2PKH is currently the most important type of transaction in the Bitcoin network, and it is necessary for beginners to understand;
- 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:
-
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
-
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
Ripemd160
of attribute variablespubKeyHash
. Corresponds to the previous P2PKH lock script<Public Key Hash>
; - Constructor
constructor
. Used to complete the initialization of attribute variables; - A custom called
unlock
public function. Parameter type respectivelySig
andPubKey
corresponding 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.js
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);
});
});
Javascript familiar friends may look to identify this is based on mocha + chai
pure 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 buildContractClass
to obtain contract DemoP2PKH
class 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 install
to 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.json
can 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 : constructorParams
and entryMethodParams
the 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.js
to 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:
- Need a private key on the testnet;
- 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.js
file, 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:
-
Create a new locked transaction:
const deployTx = await createLockingTx(privateKey.toAddress(), amountInContract)
-
Get the lock script corresponding to the contract:
const lockingScript = p2pkh.getLockingScript()
-
Set the script corresponding to the output to the above locking script:
deployTx.outputs[0].setScript(bsv.Script.fromASM(lockingScript))
-
Transaction signature:
deployTx.sign(privateKey)
-
Send the transaction to the service node:
const deployTxId = await sendTx(deployTx)
- Contract call:
-
Create a new unlock transaction:
const methodCallTx = createUnlockingTx(deployTxId, amountInContract, lockingScript, spendAmount, privateKey.toAddress())
-
Get the signature for this transaction:
const sig = signTx(methodCallTx, privateKey, lockingScript, amountInContract)
-
Get the unlocking script corresponding to the contract method call:
const unlockingScript = getUnlockingScript('unlock', sig, publicKey)
-
Set the script corresponding to the input to the above unlocking script;
methodCallTx.inputs[0].setScript(bsv.Script.fromASM(unlockingScript))
-
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
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. ↩︎
For a more detailed introduction of transaction signature (Signature), please refer to this document . ↩︎