Bitcoin Script

The SVScript() class allows you to parse existing Bitcoin Script program fragments. The developer will use this class primarily in either retrieving data fields from existing Transactions, or as a utility for building new types of Scripts.

Parsing an ASM-formatted script

When working with a local bitcoin node instance during testing, you might observe bitcoin script written as a series of plain-text OP_CODES. This is called ASM (assembly) format.

//the locking script of a Pay-to-Pubkey-Hash (P2PKH) transaction
var asm = 'OP_DUP OP_HASH160 f4c03610e60ad15100929cc23da2f3a799af1725 OP_EQUALVERIFY OP_CHECKSIG';

var script = SVScript.fromASM(asm);

//extract the pubkey hash from the parsed script
var pubKeyHash = HEX.encode(script.chunks[2].buf);

Parsing a hexadecimal-formatted script

When interacting with third-party service providers or if receiving serialized transactions over the internet, one often encounters Script instructions in their hexadecimal representations.

var script = SVScript.fromHex('76a914f4c03610e60ad15100929cc23da2f3a799af172588ac');

print(script.toString(type: 'asm'));

Custom Script Templates

With our DartSV library we have abstracted the workings of the locking and unlocking scripts away from the core of the framework. This allows for the development of novel, and custom locking/unlocking behaviour. What used to be known as “standard script templates” or “standard transactions”, are now themselves built using this API. The customization behaviour is split across three interfaces:

  • LockingScriptBuilder - defines behaviour for locking scripts
  • UnlockingScriptBuilder - defines behaviour for unlocking scripts
  • SignedUnlockBuilder - defines behaviour for unlocking scripts that contain signatures

All interactions with the Transaction class where script builders are required, will require a script that implements one, or a combination of the above interfaces.

Locking Script Builder

The LockingScriptBuilder interface forms the basis of the defined behaviour for locking scripts.

abstract class LockingScriptBuilder {
    ///This method must be implemented by all subclasses. It must return a
    ///valid locking script a.k.a scriptPubkey
    SVScript getScriptPubkey();

    ///This method must be implemented by all subclasses.
    ///
    ///The implementation of this method should be able to parse the script,
    ///and recover the internal state of the subclass. I.e. it must deserialize
    ///the locking script.
    ///
    void fromScript(SVScript script);
}

Unlocking Script Builder

The UnlockingScriptBuilder forms the basis of the defined behaviour for unlocking scripts;

abstract class UnlockingScriptBuilder {
    ///This method must be implemented by all subclasses. It must return a
    ///valid unlocking script a.k.a scriptSig
    SVScript getScriptSig();

    ///This method must be implemented by all subclasses.
    ///
    ///The implementation of this method should be able to parse the script,
    ///and recover the internal state of the subclass. I.e. it must deserialize
    ///the unlocking script.
    ///
    void fromScript(SVScript script);
}

Signed Unlocking Script Builder

All unlocking scripts that will use OP_CHECKSIG or OP_CHECKMULTISIG operations must implement this interface. Implementing this interface will signal to the framework that the unlocking script (scriptSig) will participate in the process of generating signatures for the Transaction’s Input Script.

This interface will always need to be implemented alongside the UnlockingScriptBuilder interface.

After composing the Transaction, when the developer calls the Transaction.signInput() method, the framework will inject the resultant ssgnature into the signatures property of the SignedUnlockBuilder instance. This will allow the developer to use these signatures in generating the appropriate Input Script when the subclasses’ UnlockingScriptBuilder.getScriptSig() method is called by either the framework, or explicitly by the developer themselves.

abstract class SignedUnlockBuilder {

  // The [signatures] property defines a list of signatures injected by
  // the framework whenever the Transaction.signInput() method is called.
  // Multiple sequential calls to Transaction.signInput() will cause
  // additional signatures to be added to this list.
  //
  List<SVSignature> get signatures;


  set signatures(List<SVSignature> value);
}

Default Script Builder

The TwoStack WalletSDK provides a default implementation of the above interfaces. This default implementation serves to ensure that the framework has sane internal defaults for test cases, as well as for parsing arbitrary transactions without needing to know the specifics of the script builders used to generate the scripts. The relative simplicity of the DefaultLockBuilder and DefaultUnlockBuilder classes makes an ideal candidate for explaining in a little deeper detail how one would go about building custom locking and unlocking scripts.

  • DefaultLockBuilder

    Figure 1: Default Lock Builder Implementation

    Figure 1: Default Lock Builder Implementation

    As can be seen within Figure 1, the DefaultLockBuilder class serves to tie the behaviour of the private _DefaultLockBuilder class and the public DefaultLockMixin mixin together. If you look at the source code for the DefaultLockBuilder, you will see that there is no local behaviour or state specified.

    class DefaultLockBuilder extends _DefaultLockBuilder with DefaultLockMixin{
      DefaultLockBuilder() : super();
    }
    

    This is intentional. All behaviour is delegated to either the abstract base or the mixin. The implementation of XXXLockBuilder, which the developer will be instantiating in their own code, should be primarily used for injecting state or helper classes via the constructor. An example of this can be see from the implementation of P2PKLockBuilder.

    class P2PKLockBuilder extends _P2PKLockBuilder with P2PKLockMixin{
      P2PKLockBuilder(SVPublicKey signerPubkey) : super(signerPubkey);
    }
    

    The pattern calls on the developer to :

    • Override the getScriptPubkey() method in a mixin; DefaultLockMixin in our case.
    • Implement methods and make available any state that will be needed by the mixin within the abstract base class; _DefaultLockBuilder in this case.
  • DefaultUnlockbuilder

    Figure 2: Default Unlock Builder Implementation

    Figure 2: Default Unlock Builder Implementation

    Figure [fig.DefaultUnlockBuilder] shows how the DefaultUnlockBuilder class ties the behaviour of the private _DefaultUnlockBuilder class and the public DefaultUnlockMixin mixin together. The key distinction here is the _DefaultUnlockBuilder now additionally implements the SignedUnlockBuilder interface. If you recall, classes implementing the SignedUnlockBuilder interface will participate in the Transaction signing process. These classes will have a signature injected for each call to the Transaction.signInput() method.

    The implementation of DefaultUnlockBuilder, like that of the DefaultLockBuilder remains completely devoid of local state and behaviour.

    class DefaultLockBuilder extends _DefaultLockBuilder with DefaultLockMixin{
      DefaultLockBuilder() : super();
    }
    

    The pattern of implementation is now :

    • Override the getScriptSig() method in a mixin; DefaultUnlockMixin in our case.
    • Implement methods and make available any state that will be needed by the mixin within the abstract base class; _DefaultUnlockBuilder in this case.
    • Use the access gained to the list of signatures within the mixin to implement the scriptSig appropriate for your unlocking script.

    The signatures within the *LockBuilder implementation will only become available after a call to Transaction.signInput(). This distinction is important in the cases where the developer would like to retain or save those signatures.