Back to all stories
Blogs
Tech & Dev
sCrypt: Nine Smart Contract Development Best Practices
5/1/2024

sCrypt is an embedded Domain Specific Language (eDSL) based on TypeScript. It’s designed for writing smart contracts on Bitcoin. sCrypt smart contracts, with BTC-supported opcodes, can be compiled into Bitcoin Script. The resulting assembly-like scripts are used as locking scripts in transactions.

In this article, we explore the concept behind sCrypt smart contracts, as well as some best practices and security checklists for programming with sCrypt.

sCrypt: Nine Smart Contract Development Best Practices

Smart Contracts in sCrypt: A Simple Example

Smart contracts on Bitcoin utilize the UTXO model. Each Bitcoin transaction includes inputs and outputs:

Outputs contain:

  • The amount of bitcoins.
  • Bytecodes (known as the locking script).

Inputs contain:

  • A reference to the previous transaction's output.
  • Bytecodes (the unlocking script).

An Unspent Transaction Output (UTXO) is an output that has not been consumed in any transaction. The low-level bytecode/opcode, known as Bitcoin Script, is interpreted by the Bitcoin Virtual Machine (BVM).

BTC-supported opcodes can be compiled into Bitcoin Script. The resulting assembly-like scripts are used as locking scripts in transactions.

Visual Explanation of the UTXO Model in Bitcoin Transactions

In the figure above, we have two transactions, each having one input (in green) and one output (in red). And the transaction on the right spends the one on the left. The locking script can be viewed as a boolean function, f(x), that specifies conditions for spending the bitcoins in the UTXO, serving as a lock (hence "locking"). The unlocking script provides the function arguments that cause f to evaluate to true, acting as the "key" (also called witness) to unlock it. Only when the "key" in an input matches the "lock" of the previous output can it spend the bitcoins contained in that output.

In a standard Bitcoin payment to a Bitcoin address, the locking script is Pay To Pubkey Hash (P2PKH). It verifies that the spender possesses the correct private key corresponding to the address, enabling them to produce a valid signature in the unlocking script. The sCrypt language enables the locking script to specify arbitrarily more complex spending conditions than simple P2PKH, i.e., Bitcoin smart contracts in P2TR/P2SH transactions.

Smart contracts in sCrypt are conceptually similar to a class in Object Oriented Programming. Each package provides a template for a certain type of contracts (e.g., P2PKH or multisig), which can be instantiated into concrete runnable contract objects.

Sample sCrypt Smart Contract Code for Equation Verification

Deploying and Calling a sCrypt Smart Contract

sCrypt uses Pay-to-Witness-Script-Hash (P2WSH) for contract deployment. The deployment includes compiling the smart contract code to generate a script, hashing this script, and then placing the hash value into a P2WSH transaction (Tx0), which is broadcast to the network.

When someone wants to call a deployed contract, they embed the complete contract script along with the inputs for the called method as witness data in a subsequent transaction (Tx1) that spends Tx0.

Diagram of Pay-to-Witness-Script-Hash (P2WSH) Transaction Process Deployment and Transaction Calls: The left side represents inputs, and the right side represents outputs.

Known Limitations of sCrypt

sCrypt can work on any blockchain that supports Bitcoin Script. This includes Bitcoin forks and Bitcoin-derived chains such as Litecoin and Doge.

BTC has disabled many Script opcodes such as OP_CAT and OP_MUL, greatly limiting the types of smart contracts that can be expressed in sCrypt. The BTC community is actively discussing re-enabling such opcodes and introducing new ones, which will make sCrypt on BTC more powerful than it is today if the proposed changes are accepted.

In the meantime, there are chains that have the full suite of Script opcodes, like Bitcoin SV and MVC. sCrypt reaches its full capacity on these chains today.

The Back-to-Genesis Problem

Implementing fungible and non-fungible tokens with sCrypt smart contracts introduces the Back-to-Genesis (B2G) problem, a notable security challenge. This issue entails tracing a token's creation transaction within a UTXO-based blockchain. On blockchains like Bitcoin, tokens created with sCrypt are represented as UTXOs and may change hands frequently. The B2G problem surfaces when attempting to track or verify a token's entire history, including its genesis transaction, which is crucial for validating its provenance and authenticity.

This can be important for various reasons, including:

  • Provenance and Authenticity: Knowing a token's entire history allows users to verify its authenticity and provenance. Users may want to ensure that a token has a legitimate and traceable origin.
  • Smart Contract Logic: Smart contracts or decentralized applications may have logic that depends on the token's history. Knowing the origin of a token can be crucial for certain smart contract functionalities.

There are two methods to forge sCrypt-based fungible and NFT tokens, illustrated in the diagram below. Each box represents a transaction with inputs on the left and outputs on the right. Arrows indicate the flow from one transaction to another. Transactions with the same output color utilize identical contract codes.

Blockchain Transaction Chain Illustrating Security Attacks Output on the right. An arrow points from one transaction to the transaction it spends. Boxes of the same output color contain the same contract code.

The Back-to-Genesis problem can lead to two issues within the token protocol:

  • Replay Attack: An attacker might reissue the same token.
  • Man-in-the-Middle Attack: An attacker could start from an unrelated UTXO and replicate a token transaction chain. They could copy a transaction's output (as shown in row 1) and paste it verbatim into another transaction (as shown in row 3). This is possible because the locking script is only evaluated upon unlocking, not during deployment. From then on, the transaction can be spent creating a parallel fake token chain.

Solutions

In these scenarios, transactions are accepted by miners because they meet layer-1 validations. The last transactions, marked in red circles, appear identical. The only method to verify the legitimacy of a token transaction is to trace it back to its issuance transaction (the Origin).

To address replay attacks, we recommend implementing a globally unique ID, “GenesisID,” which represents the transaction ID (txid) of the issuance transaction. This ID is copied when the issuance UTXO is consumed and preserved in all subsequent token transfer UTXOs as the token ID.

Blockchain Transaction Chains with Unique Token IDs

To mitigate man-in-the-middle attacks, we suggest developers backtrace two steps prior to the current transaction, validating both the parent transaction and its predecessor.

Here is a simple example to illustrate the backtrace validation and why validating two steps ahead is important:

Blockchain Validation Strategy: Two Steps Ahead

When the forged UTXO (UTXO1) is spent into another token UTXO (UTXO2), it passes miner validation because the token contract is not activated (the locking script of a UTXO is only executed upon unlocking). However, in our suggested implementation, attempting to spend UTXO2 in UTXO3 involves validating both UTXO1 and UTXO2. Thus, the man-in-the-middle attack fails because UTXO1 does not contain the same unlocking contract as UTXO2 and UTXO3, and the transaction will be rejected as a result.

sCrypt Security Tips and Checklists

The following tips and checklists are what we learned and summarized after finishing the audit of an sCrypt-based fungible/non-fungible token project:

1: Verify that the token's backtrace is accurate and that the current UTXO's code segment of the locking script matches that of the previous UTXO.

The sCrypt team has provided an example that can help developers design a backtrace validation flow:

Detailed Blockchain Transaction Validation Flowchart

2: Ensure that the protocol cannot be compromised by a forged Genesis ID.

This can be achieved by validating the Genesis ID during the backtrace process, as illustrated in the following example:

bytes genesisTxid = TokenProto.getGenesisTxid(tokenScript, tokenScriptLen); if (genesisTxid != hash256(prevTokenTxProof.txHeader) + tokenTxInputProof.outputIndexBytes) { // backtrace to genesis contract bytes genesisHash = TokenProto.getGenesisHash(tokenScript, tokenScriptLen); bool backtraceGenesis = (genesisHash == ripemd160(prevTokenTxProof.scriptHash)); // backtrace to token contract // verify prev token script data and script code bytes prevTokenScript = TokenProto.getNewTokenScript(tokenScript, tokenScriptLen, prevTokenAddress, prevTokenAmount); bool backtraceToken = (sha256(prevTokenScript) == prevTokenTxProof.scriptHash); require(backtraceGenesis || backtraceToken); }

The check should confirm that the current genesisTxid matches either the genesis transaction's ID or the ID from the previous transaction.

3: Confirm that the UTXO Input Proof is authentic and not forged.

The following parameters must be validated:

  • tokenTxHeader
  • prevTokenInputIndex
  • tokenTxInputProof
  • prevTokenTxProof

We recommend developers follow this flowchart when validating UTXO input proofs.

Complex Blockchain Transaction Validation Flowchart

4: Ensure that the token unlocking process has been properly authorized.

Tokens can be unlocked by the token owner directly or through a token-locking contract. Token owners must verify their ownership with a valid signature to unlock their tokens. If tokens are unlocked via a smart contract, it is crucial to adhere to any stipulations or restrictions specified in the contract. Note that unlocking burned tokens is prohibited.

5: Ensure that the token amount in token transfer UTXOs remains consistent to prevent double-spending.

Unlocked tokens could be transferred to one or more recipient wallets. It is important to ensure that the total amount of tokens used as inputs equals the total amount spent as outputs. In other words, the tokens being transferred must match the tokens available for spending, maintaining a balance. By enforcing this requirement, the contract prevents the possibility of double-spending.

6: Verify that the appropriate SigHash type is selected, specifying which components of the transaction are signed.

SigHash flags determine which parts of a Bitcoin transaction are covered by the cryptographic signature. By default, the SIGHASH_ALL flag is used, ensuring the signature covers all inputs and outputs. However, selecting different SigHash types requires caution. For instance, the SIGHASH_NONE flag, which signs none of the outputs, may introduce security vulnerabilities. Alterable outputs after signature application could lead to fraud or manipulation.

7: Verify contract integrity.

In Bitcoin's UTXO model, smart contracts are typically one-off and stateless. This is because a UTXO that contains a contract is destroyed once spent, leaving no blockchain trace. This design, while simple and efficient, poses security risks as contracts can be tampered with. To mitigate this, verify the contract script code hash, which involves embedding a hash of the script within the script data itself. During a transaction, comparing the embedded hash with the script's actual hash confirms the script's integrity.

To address this challenge, one approach involves verifying the contract script code hash. This method entails saving a hash of the script as part of the data within the script itself. During the execution of a transaction involving a smart contract, the script code hash can be verified against its stored value. If the hash values match, it confirms that the contract script remains unchanged and has not been tampered with.

8: Verify transaction integrity.

sCrypt comes with a powerful library called Tx that enables the inspection of the entire transaction containing the contract itself, in addition to the locking script and unlocking script. sCrypt empowers contracts to validate inputs by leveraging this comprehensive transaction inspection feature.

One of the primary verification steps involves matching inputs to the unlock script with the data extracted from the transaction preimage. Moreover, validating the txPreimage as the preimage of the current transaction is required.

9: Verify data integrity.

Within the locking script of an output, smart contracts are divided into two components: code and state. The contract's state is stored within the data part of the locking script. In scenarios where the protocol itself manages the data section, special attention must be paid to the handling of data fields. It's essential to verify that the data fields stored within the data section of the lock script are accessed and stored at the correct location index.

;