All pages
Powered by GitBook
1 of 14

Python

Welcome to the BSV SDK! This guide is tailored for developers working in a Python environment. We'll walk you through the installation process and show you how to get started with creating and signing a Bitcoin SV transaction using the SDK. Whether you're building on BSV for the first time or transitioning an existing project to use the SDK, this guide is for you.

Prerequisites

Before we begin, make sure you have Python installed on your system. You can download and install Python from python.org/. This guide assumes you have basic knowledge of working with Python.

Installation

First, you'll need to install the BSV SDK package in your environment. Open your terminal and run the following command:

pip install bsv-sdk

This command installs the BSV SDK in your environment, making it ready for use. There are no external runtime dependencies.

Requiring the SDK

To use the BSV SDK in a Python project, you'll import modules using the import syntax. Here's how you set up a basic script to use the BSV SDK:

  1. Create a new Python file in your project. For example, main.py.

  2. At the top of your file, require the SDK modules along with other modules you plan to use. For instance:

import asyncio  # Needed for calling async methods...
from bsv import (
    PrivateKey, P2PKH, Transaction, TransactionInput, TransactionOutput
)

Creating and Signing a Transaction

Now, let's create and sign a transaction. We'll follow the example provided in the README. This example demonstrates how to create a transaction from a source to a recipient, including calculating fees, signing the transaction, and broadcasting it.

Copy and paste the following code into your main.py file below your import statements:

# Replace with your private key (WIF format)
PRIVATE_KEY = 'KyEox4cjFbwR---------VdgvRNQpDv11nBW2Ufak'

# Replace with your source tx which contains UTXO that you want to spend (raw hex format)
SOURCE_TX_HEX = '01000000018128b0286d9c6c7b610239bfd8f6dcaed43726ca57c33aa43341b2f360430f23020000006b483045022100b6a60f7221bf898f48e4a49244e43c99109c7d60e1cd6b1f87da30dce6f8067f02203cac1fb58df3d4bf26ea2aa54e508842cb88cc3b3cec9b644fb34656ff3360b5412102cdc6711a310920d8fefbe8ee73b591142eaa7f8668e6be44b837359bfa3f2cb2ffffffff0201000000000000001976a914dd2898df82e086d729854fc0d35a449f30f3cdcc88acce070000000000001976a914dd2898df82e086d729854fc0d35a449f30f3cdcc88ac00000000'

async def create_and_broadcast_transaction():
    priv_key = PrivateKey(PRIVATE_KEY)
    source_tx = Transaction.from_hex(SOURCE_TX_HEX)

    tx_input = TransactionInput(
        source_transaction=source_tx,
        source_txid=source_tx.txid(),
        source_output_index=1,
        unlocking_script_template=P2PKH().unlock(priv_key),
    )

    tx_output = TransactionOutput(
        locking_script=P2PKH().lock(priv_key.address()),
        change=True
    )

    tx = Transaction([tx_input], [tx_output], version=1)

    tx.fee()
    tx.sign()

    await tx.broadcast()

    print(f"Transaction ID: {tx.txid()}")
    print(f"Raw hex: {tx.hex()}")

if __name__ == "__main__":
    asyncio.run(create_and_broadcast_transaction())

This script demonstrates the entire process of creating a transaction, from initializing keys to signing and broadcast. When you run this script using Python (replacing the source transaction and private key), the spend will be signed and broadcast to the BSV network.

Running Your Script

To run your script, simply execute the following command in your terminal:

python main.py

Conclusion

Congratulations! You've successfully installed the BSV SDK in your Python project and created a signed transaction. This guide covered the basics to get you started, but the BSV SDK is capable of much more. Explore the SDK documentation for detailed information on all the features and functionalities available to build scalable applications with the BSV blockchain.

Examples

Simple Tx

This guide walks you through the steps of creating a simple Bitcoin transaction. To get started, let's explain some basic concepts around Bitcoin transactions.

Understanding and Creating Transactions

Transactions in Bitcoin are mechanisms for transferring value and invoking smart contract logic. The Transaction class in the BSV SDK encapsulates the creation, signing, and broadcasting of transactions, also enabling the use of Bitcoin's scripting language for locking and unlocking coins.

Creating and Signing a Transaction

Consider the scenario where you need to create a transaction. The process involves specifying inputs (where the bitcoins are coming from) and outputs (where they're going). Here's a simplified example:

from bsv import PrivateKey, PublicKey, ARC, P2PKH, Transaction

priv_key = PrivateKey.fromWif('...')        # Your P2PKH private key
change_priv_key = PrivateKey.fromWif('...') # Change private key (never re-use addresses)
recipient_address = '1Fd5F7XR8LYHPmshLNs8cXSuVAAQzGp7Hc' # Address of the recipient

tx = Transaction()

# Add the input
tx.add_input(
    TransactionInput(
        source_transaction=Transaction.from_hex('...'), # The source transaction where the output you are spending was created
        source_output_index=0,
        unlocking_script_template=P2PKH().unlock(priv_key), # The script template you are using to unlock the output, in this case P2PKH
    )
)

# Pay an output to a recipient using the P2PKH locking template
tx.add_output(
    TransactionOutput(
        locking_script=P2PKH().lock(recipient_address),
        satoshis=2500
    )
)

# Send remainder back the change address
tx.add_output(
    TransactionOutput(
        lockingScript=P2PKH().lock(change_priv_key.address()),
        change=True
    )
)

# Now we can compute the fee and sign the transaction
tx.fee()
tx.sign()

# Finally, we broadcast it with ARC.
# get your api key from https://console.taal.com
api_key = 'mainnet_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' # replace
await tx.broadcast(ARC('https://api.taal.com/arc', api_key))

This code snippet demonstrates creating a transaction, adding an input and an output, setting a change script, configuring the fee, signing the transaction, and broadcasting with the ARC broadcaster. It uses the P2PKH Template, which is a specific type of Bitcoin locking program. To learn more about templates, check out this example (link to be provided once complete).

Handling Hex Locking Scripts

Moving beyond this basic example into more advanced use-cases enables you to start dealing with custom scripts. If you're provided with a hex-encoded locking script for an output, you can set it directly in the transaction's output as follows:

transaction.add_output(
    TransactionOutput(
        locking_script=Script.from_asm('OP_ADD OP_MUL ...'), 
        satoshis=100
    )
)

The Transaction class abstracts the complexity of Bitcoin's transaction structure. It handles inputs, outputs, scripts, and serialization, offering methods to easily modify and interrogate the transaction. Check out the full code-level documentation, refer to other examples, or reach out to the community to learn more.

Verifying BEEF

The BSV SDK comes with advanced capabilities around the SPV architecture. In Bitcoin, SPV refers to the process of creating, exchanging, and verifying transactions in a way that anchors them to the blockchain. One of the standard formats for representing the necessary SPV data is known as BEEF (background-evaluated extended format), representing a transaction and its parents together with needed merkle proofs. This example will show you how to verify the legitimacy of a BEEF-formatted SPV structure you've received, checking to ensure the target transaction and its ancestors are well-anchored to the blockchain based on the current chain of block headers. First we'll unpack these concepts, then we'll dive into the code.

Block Headers and Merkle Proofs

In Bitcoin, miners create blocks. These blocks comprise merkle trees of the included transactions. The root of the merkle tree, together with other useful information, is collected together into a block header that can be used to verify the block's proof-of-work. This merkle tree structure enables anyone to keep track of the chain of block headers without keeping a copy of every Bitcoin transaction.

Merkle proofs are simply a way for someone to prove the existence of a given transaction within a given merkle tree, and by extension, its inclusion by the miners in a particular block of the blockchain. This becomes extremely important and useful when we think about Simplified Payment Verification (SPV).

Simplified Payment Verification (SPV)

The process for SPV is detailed in BRC-67, but the main idea is that when a sender sends a transaction, they include merkle proofs on all of the input transactions. This allows anyone with a copy of the Bitcoin block headers to check that the input transactions are included in the blockchain. Verifiers then check that all the input and output scripts correctly transfer value from one party to the next, ensuring an unbroken chain of spends. The BEEF data structure provides a compact and efficient way for people to represent the data required to perform SPV.

Block Headers Client

To verify BEEF structures with the BSV SDK, you'll need to provide a chain tracker that, given a merkle root, will indicate to the library whether the merkle root is correct for the block that's in the active chain at the given block height.

For simplicity in this example, we are going to use a mock headers client that always indicates every merkle root as valid no matter what. However, in any real project, you MUST always use an actual chain tracker or attackers will be able to easily fool you with fraudulent transactions!

Here is the gullible chain tracker we will be using:

class GullibleChainTracker(ChainTracker):

    async def is_valid_root_for_height(self, root: str, height: int) -> bool:
        return True   # DO NOT USE IN PRODUCTION!

Verifying a BEEF Structure

Now that you have access to a block headers client (either Pulse on a real project or the above code for a toy example), we can proceed to verifying the BEEF structure with the following code:

# Replace with the BEEF structure you'd like to check
BEEF_hex = '0100beef01fe636d0c0007021400fe507c0c7aa754cef1f7889d5fd395cf1f785dd7de98eed895dbedfe4e5bc70d1502ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e010b00bc4ff395efd11719b277694cface5aa50d085a0bb81f613f70313acd28cf4557010400574b2d9142b8d28b61d88e3b2c3f44d858411356b49a28a4643b6d1a6a092a5201030051a05fc84d531b5d250c23f4f886f6812f9fe3f402d61607f977b4ecd2701c19010000fd781529d58fc2523cf396a7f25440b409857e7e221766c57214b1d38c7b481f01010062f542f45ea3660f86c013ced80534cb5fd4c19d66c56e7e8c5d4bf2d40acc5e010100b121e91836fd7cd5102b654e9f72f3cf6fdbfd0b161c53a9c54b12c841126331020100000001cd4e4cac3c7b56920d1e7655e7e260d31f29d9a388d04910f1bbd72304a79029010000006b483045022100e75279a205a547c445719420aa3138bf14743e3f42618e5f86a19bde14bb95f7022064777d34776b05d816daf1699493fcdf2ef5a5ab1ad710d9c97bfb5b8f7cef3641210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000001000100000001ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000000'

# You can create a Transaction from BEEF hex directly
tx = Transaction.from_beef(BEEF_hex)
print('TXID:', tx.txid())

# This ensures the BEEF structure is legitimate
verified = await tx.verify(GullibleChainTracker())

# Print the results
print('BEEF verified:', verified)

The above code allows you to ensure that a given BEEF structure is valid according to the rules of SPV.

Complex Tx

In Bitcoin, transactions contain inputs and outputs. The outputs are locked with scripts, and the inputs redeem these scripts by providing the correct unlocking solutions. This guide will show you how to create transactions that make use of custom inputs, outputs, and the associated script templates. For a more straightforward example, check out how to create a simpler transaction.

Transaction Input and Outputs

All Bitcoins are locked up in Bitcoin transaction outputs. These outputs secure the coins by setting constraints on how they can be consumed in future transaction inputs. This security mechanism makes use of "scripts" — programs written in a special predicate language. There are many types of locking programs, embodying the multitude of BSV use-cases. The BSV SDK ships with a script templating system, making it easy for developers to create various types of scripts and abstracting away the complexity for end-users. You can learn about script templates in the example.

Creating a Transaction

To create a transaction with the SDK, you can either use the constructor:

  • Use the constructor and pass in arrays of inputs and outputs, or

  • Construct a blank transaction, then call the add_input and add_output methods

tx = Transaction(version, inputs_list, outputs_list, lock_time)
# or
tx = Transaction()
    .add_input(inputA)
    .add_input(inputB)
    .add_output(outputA)
tx.version = version
tx.lock_time = lock_time

Note that the version and lock time parameters are optional.

Adding Inputs and Outputs

When constructing a Bitcoin transaction, inputs and outputs form the core components that dictate the flow of bitcoins. Here’s how to structure and add them to a transaction:

Transaction Inputs

An input in a Bitcoin transaction represents the bitcoins being spent. It's essentially a reference to a previous transaction's output. Inputs have several key components:

  • source_transaction or source_txid: A reference to either the source transaction (another Transaction instance), its TXID. Referencing the transaction itself is always preferred because it exposes more information to the library about the outputs it contains.

  • source_output_index: A zero-based index indicating which output of the referenced transaction is being spent.

  • sequence: A sequence number for the input. It allows for the replacement of the input until a transaction is finalized. If omitted, the final sequence number is used.

  • unlocking_script: This script proves the spender's right to access the bitcoins from the spent output. It typically contains a digital signature and, optionally, other data like public keys.

  • unlocking_script_template: A template that provides a method to dynamically generate the unlocking script.

Next, we'll add an R-puzzle input into this transaction using the `RPuzzle`` template.

Example: Adding an Input

source_transaction = Transaction.from_hex('...')
puz = RPuzzle()
k = 1
unlocking_script_template = puz.unlock(k)
tx_input = TransactionInput(
    source_transaction=source_transaction,
    source_output_index=0
    unlocking_script_template=unlocking_script_template
)

my_tx = Transaction()
my_tx.add_input(tx_input)

Transaction Outputs

Outputs define where the bitcoins are going and how they are locked until the next spend. Each output includes:

  • satoshis: The amount of satoshis (the smallest unit of Bitcoin) being transferred. This value dictates how much value the output holds.

  • locking_script: A script that sets the conditions under which the output can be spent. It's a crucial security feature, often requiring a digital signature that matches the recipient's public key.

  • change: An optional boolean flag indicating if the output is sending change back to the sender.

We will now add an R-puzzle output to a transaction, making use of the script template.

Example: Adding an Output

# We must first obtain an R-value for the template
pubkey = PublicKey('...')

G: Point = curve.g
r = curve_multiply(k, G).x % curve.n

r_bytes = r.to_bytes(32, byteorder='big')
if r_bytes[0] > 0x7f:
    r_bytes = b'\x00' + r_bytes

puz = RPuzzle()
locking_script = puz.lock(r)

let tx_output = {
    satoshis=1000,  # Amount in satoshis
    locking_script=locking_script,
    change=False    # Not a change output, it has a defined number of satoshis
}

my_tx.add_output(tx_output)

Change and Fee Computation

The transaction fee is the difference between the total inputs and total outputs of a transaction. Miners collect these fees as a reward for including transactions in a block. The amount of the fee paid will determine the quality of service provided my miners, subject to their policies.

If the total value of the inputs exceeds the total value you wish to send (plus the transaction fee), the excess amount is returned to you as "change." Change is sent back to a destination controlled by the sender, ensuring that no value is lost. When you set the change property on an output to true, you don't need to define a number of satoshis. This is because the library computes the number of satoshis for you, when the .fee() method is called.

In summary:

  1. After all funding sources and recipient outputs are added, add at least one output where change is true, so that you capture what's left over after you send. Set up a locking script you control so that you can later spend your change.

  2. Then, call the .fee() method to compute the change amounts across all change outputs, and leave the rest to the miner. You can specify a custom fee model if you wish, but the default should suffice for most use-cases.

In our above code, we already added a change output — now, we can just compute the fees before transaction signing.

# Compute the correct amounts for change outputs and leave the rest for the Bitcoin miners
my_tx.fee()

Signing and Signature Validity

Once you've defined your inputs and outputs, and once your change has been computed, the next step is to sign your transaction. There are a few things you should note when signing:

  • Only inputs with an unlocking template will be signed. If you provided an unlocking script yourself, the library assumes the signatures are already in place.

  • If you change the inputs or outputs after signing, certain signatures will need to be re-computd, depending on the SIGHASH flags used.

  • If your templates support it, you can produce partial signatures before serializing and sending to other parties. This is especially useful for multi-signature use-cases.

With these considerations in mind, we can now sign our transaction. The RPuzzle unlocking templates we configured earlier will be used in this process.

# Set the input unlocking scripts based on the script templates
my_tx.sign()

Serialization and Broadcast

After a transaction is signed, it can be broadcast to the BSV Mining Network, or to relevant Overlay Networks through the SDK.

# get your api key from https://console.taal.com
api_key = 'mainnet_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' # TODO: replace
await tx.broadcast(ARC('https://api.taal.com/arc', api_key))

Alternatively, if you don't want to use the SDK's built-in broadcasting system, you can simply serialize your transaction into a hex string as follows:

# Serialize your transaction
my_tx.hex()

SPV and Serialization Formats

Simplified Payment Verification is a mechanism that enables the recipient of a transaction to verify its legitimacy by providing necessary information, like input transactions and their associated merkle proofs.

Earlier in this guide, we mentioned that you can either reference a source_txid or, preferably, a source_transaction when linking transaction inputs. The reason why it's preferable to link the entire source transaction is because serializing the transaction in an SPV-compliant way generally requires more information about the outputs being spent.

When properly linked, you can serialize your transactions in the SPV formats as follows:

# Note: Requires use of source_transaction instead of source_txid for inputs
my_tx.to_BEEF()
# or
my_tx.to_EF()

This enables the transactions to be verified properly by recipients, using the .verify() method:

incoming_tx = Transaction.from_BEEF('...')
incoming_tx.verify(chain_tracker) # Provide a source of BSV block headers to verify

Recipients, with nothing other than a source of BSV block headers, can verify that the transaction properly unlocks and redeems its inputs, thereby creating its outputs. To learn more about setting up a chain tracker with a source of block headers, check out the Pulse example (link to be provided once completed).

Script Templates

This guide will provide information about the structure and functionality of script templates within the BSV SDK. Script templates are a powerful abstraction layer designed to simplify the creation and management of the scripts used in Bitcoin transactions. By understanding how these templates work, developers can leverage them to build more sophisticated and efficient blockchain applications. By the end of this example, you'll understand how the R-puzzle script template (P2RPH) was created.

Understanding Script Templates

A script template is essentially a blueprint for creating the locking and unlocking scripts that are crucial for securing and spending bitcoins. These templates encapsulate the logic needed to construct these scripts dynamically, based on the parameters passed to them. This approach allows for a modular and reusable codebase, where common scripting patterns can be defined once and then instantiated as needed across different transactions.

Locking Script

The locking script, or output script, specifies the conditions under which the bitcoins can be spent. In the BSV SDK, the lock function of a script template is responsible for generating this script. By abstracting the creation of locking scripts into a method that accepts parameters, developers can easily create diverse conditions for spending bitcoins without having to write the low-level script code each time.

For example, a locking script might require the presentation of a public key that matches a certain hash or the fulfillment of a multi-signature condition. The flexibility of passing parameters to the lock function enables the creation of locking scripts tailored to specific requirements. This example will require a signature created with a particular ephemeral K-value, an R-puzzle.

Unlocking Script

The unlocking script, or input script, provides the evidence needed to satisfy the conditions set by the locking script. The unlock method in a script template not only generates this script but also offers two key functionalities — it's a function that returns an object with two properties:

  1. estimate_length: Before a transaction is signed and broadcast to the network, it's crucial to estimate its size to calculate the required fee accurately. The estimateLength function predicts the length of the unlocking script once it will be created, allowing developers to make informed decisions about fee estimation.

  2. sign: This function generates an unlocking script that includes the necessary signatures or data required to unlock the bitcoins. By accepting a transaction and an input index as arguments, it ensures that the unlocking script is correctly associated with the specific transaction input it intends to fund, allowing signatures to be scoped accordingly.

Creating a Script Template

To create a script template, developers define a class that adheres to the ScriptTemplate interface. This involves implementing the lock and unlock methods with the specific logic needed for their application.

Now that you understand the necessary components, here's the code for the R-puzzle script template:

class RPuzzle(ScriptTemplate):
    
    def __init__(self, puzzle_type: str = 'raw'):
        """
        Constructs an R Puzzle template instance for a given puzzle type.

        :param puzzle_type: Denotes the type of puzzle to create ('raw', 'SHA1', 'SHA256', 'HASH256', 'RIPEMD160', 'HASH160')
        """
        assert(puzzle_type in ['raw', 'SHA1', 'SHA256', 'HASH256', 'RIPEMD160', 'HASH160'])
        self.type = puzzle_type

    def lock(self, value: bytes) -> Script:
        """
        Creates an R puzzle locking script for a given R value or R value hash.

        :param value: A byte array representing the R value or its hash.
        :returns: An R puzzle locking script.
        """
        chunks = [
            OpCode.OP_OVER,
            OpCode.OP_3,
            OpCode.OP_SPLIT,
            OpCode.OP_NIP,
            OpCode.OP_1,
            OpCode.OP_SPLIT,
            OpCode.OP_SWAP,
            OpCode.OP_SPLIT,
            OpCode.OP_DROP
        ]
        if self.type != 'raw':
            chunks.append(getattr(OpCode, f'OP_{self.type}'))
        chunks.append(encode_pushdata(value))
        chunks.append(OpCode.OP_EQUALVERIFY)
        chunks.append(OpCode.OP_CHECKSIG)
        return Script(b''.join(chunks))
    
    
    def unlock(self, k: int, private_key: Optional[PrivateKey] = PrivateKey(), sign_outputs: str = 'all', anyone_can_pay: bool = False):
        """
        Creates a function that generates an R puzzle unlocking script along with its signature and length estimation.

        :param k: The K-value used to unlock the R-puzzle.
        :param private_key: The private key used for signing the transaction.
        :param sign_outputs: The signature scope for outputs ('all', 'none', 'single').
        :param anyone_can_pay: Flag indicating if the signature allows for other inputs to be added later.
        :returns: An object containing the `sign` and `estimate_length` functions.
        """
        def sign(tx: Transaction, input_index: int) -> Script:
            sighash = SIGHASH.FORKID
            if sign_outputs == 'all':
                sighash |= SIGHASH.ALL
            elif sign_outputs == 'none':
                sighash |= SIGHASH.NONE
            elif sign_outputs == 'single':
                sighash |= SIGHASH.SINGLE
            if anyone_can_pay:
                sighash |= SIGHASH.ANYONECANPAY
                
            tx.inputs[input_index].sighash = sighash

            preimage = tx.preimage(input_index)

            sig = private_key.sign(preimage, hasher=hash256, k=k) + sighash.to_bytes(1, "little")
            pubkey_for_script = private_key.public_key().serialize()

            return Script(encode_pushdata(sig) + encode_pushdata(pubkey_for_script))

        def estimated_unlocking_byte_length() -> int:
            # public key (1+33) + signature (1+71)
            # Note: We add 1 to each element's length because of the associated OP_PUSH
            return 106

        return to_unlock_script_template(sign, estimated_unlocking_byte_length)

In this example, RPuzzle defines custom logic for creating both locking and unlocking scripts. The opcodes, intermixed with the various template fields, enable end-users to implement R-puzzles into their applications without being concerned with these low-level details. Check out this guide to see an example of this template used in a transaction.

Conclusion

Script templates in the BSV SDK offer a structured and efficient way to handle the creation of locking and unlocking scripts in Bitcoin transactions. By encapsulating the logic for script generation and providing essential functionalities like signature creation and length estimation, script templates make it easier for developers to implement complex transactional logic. With these tools, template consumers can focus on the higher-level aspects of their blockchain applications, relying on the SDK to manage the intricacies of script handling.

Encryption

This guide walks you through the steps of encrypting and decrypting messages.

Overview

Understanding the ins-and-outs of message encryption and decryption is key to implementing secure communication. The implemented functions allow a sender to encrypt messages that only the intended recipient can decrypt, thus preserving the privacy of the message exchange.

Encrypting a Message

To get started, you will first want to import the required functions / classes.

from bsv import PrivateKey

Next, you will want to configure who the sender is, the recipient, and what message you would like to encrypt.

sender = PrivateKey()
recipient = Private()

message = 'The cake is a lie.'

Now you are ready to generate the ciphertext using the encrypt_text function.

encrypted = sender.public_key().encrypt_text(message)
print(encrypted)

Decrypting a Message

To get back your plaintext message, use the decrypt_text function and then transform it as needed.

print(recipient.decrypt_text(encrypted))

Considerations

As you leverage encryption and decryption in your applications, it's crucial to remember:

  • Key management: Private keys should always be kept safe and never exposed. This library insures that intermediate keys generated for message encryption always use random nonces to mitigate key-reuse attack vectors.

  • Key of recipient: The decryption stage using the recipient's key is always done on the recipient's end. As such, the recipient key used is the public key provided by the recipient, and is just generated in the above example for testing purposes.

  • Interpretation of decrypted data: The decrypted byte array may need to be transformed back into a string, depending on the nature of the original message. There are several utility functions exported, such as toUTF8, that can be leveraged as needed.

This is just a simple example of how encryption and decryption can be implemented for secure message exchange in your applications. The exact method of implementation might vary based on your application’s specifics.

Message Signing

This guide walks through the necessary steps for both public and private message signing.

Overview

Message signing is a mechanism that preserves the integrity of secure communications, enabling entities to verify the authenticity of a message's origin. This document emphasizes two primary types of message signing: private and public.

Private message signing is used when a message needs to be verified by a specific recipient. In this scenario, the sender creates a unique signature using their private key combined with the recipient's public key. The recipient, using their private key, can then verify the signature and thereby the message's authenticity.

On the other hand, public message signing creates a signature that can be verified by anyone, without the need for any specific private key. This is achieved by the sender using only their private key to create the signature.

The choice between private and public message signing hinges on the specific requirements of the communication. For applications that require secure communication where authentication is paramount, private message signing proves most effective. Conversely, when the authenticity of a message needs to be transparent to all parties, public message signing is the go-to approach. Understanding these differences will enable developers to apply the correct method of message signing depending on their specific use case.

1. Example Code - Private Message Signing

To get started, you will first want to import the required functions / classes.

from bsv import PrivateKey, verify_signed_text

Next, you will want to configure who the sender is, the recipient, and what message you would like to sign.

sender = PrivateKey()
recipient = PrivateKey()

message = 'Hello world!'

Now we can sign the message and generate a signature that can only be verified by our specified recipient.

address, signature = sender.sign_text(message)

verified = verify_signed_text(message, address, signature)
print('Verified:': verified)

2. Example Code - Public Message Signing

To create a signature that anyone can verify, the code is very similar to the first example, just without a specified recipient. This will allow anyone to verify the signature generated without requiring them to know a specific private key.

signer = PrivateKey()

message = 'I like big blocks and I cannot lie!'

address, signature = signer.sign_text(message)
verified = verify_signed_text(message, address, signature)
print('Verified:': verified)

Considerations

While these private signing functions are built on industry standards and well-tested code, there are considerations to keep in mind when integrating them into your applications.

  • Private Key Security: Private keys must be stored securely to prevent unauthorized access, as they can be used to sign fraudulent messages.

  • Use Case Analysis: As stated in the overview, you should carefully evaluate whether you need a private or publicly verifiable signature based on your use case.

  • Implications of Signature Verifiability: When creating signatures that anyone can verify, consider the implications. While transparency is achieved, sensitive messages should only be verifiable by intended recipients.

By understanding and applying these considerations, you can ensure a secure implementation of private signing within your applications.

Building A Custom Broadcaster

This guide walks through the necessary steps for building a custom transaction broadcast client.

Overview

A transaction broadcast client is a crucial component in any Bitcoin SV application, allowing it to communicate with the Bitcoin SV network. Implementing a transaction broadcaster can be accomplished using the clearly defined Broadcast interface.

Our task will be to create a broadcaster that connects with the What's on Chain service. This broadcaster is particularly designed for browser applications and utilizes the standard Fetch API for HTTP communications with the relevant API endpoints.

Getting Started

In order to build a compliant broadcast client, we first need to import the interfaces to implement.

from bsv import (
    Broadcaster,
    BroadcastFailure,
    BroadcastResponse,
    Transaction,
    HttpClient,
    default_http_client,
)

Next, we create a new class that implements the Broadcaster interface which requires a broadcast function.

We will be implementing a What's on Chain (WOC) broadcaster that runs in a browser context and uses window.fetch to send a POST request to the WOC broadcast API endpoint.

class WOC(Broadcaster):

    def __init__(self, network: str = "main", http_client: HttpClient = None):
        """
        Constructs an instance of the WOC broadcaster.

        :param network: which network to use (test or main)
        :param http_client: HTTP client to use. If None, will use default.
        """
        self.network = network
        self.URL = f"https://api.whatsonchain.com/v1/bsv/{network}/tx/raw"
        self.http_client = http_client if http_client else default_http_client()

    async def broadcast(
        self, tx: Transaction
    ) -> Union[BroadcastResponse, BroadcastFailure]:
        """
        Broadcasts a transaction via WOC.

        :param tx: The transaction to be broadcasted as a serialized hex string.
        :returns: BroadcastResponse or BroadcastFailure.
        """
        request_options = {
            "method": "POST",
            "headers": {"Content-Type": "application/json", "Accept": "text/plain"},
            "data": {"txhex": tx.hex()},
        }

        try:
            response = await self.http_client.fetch(self.URL, request_options)
            if response.ok:
                txid = response.json()["data"]
                return BroadcastResponse(
                    status="success", txid=txid, message="broadcast successful"
                )
            else:
                return BroadcastFailure(
                    status="error",
                    code=str(response.status_code),
                    description=response.json()["data"],
                )
        except Exception as error:
            return BroadcastFailure(
                status="error",
                code="500",
                description=(str(error) if str(error) else "Internal Server Error"),
            )

Now, you can make use of this broadcast client when sending transactions with the .broadcast() method:

await tx.broadcast(WOC("test"))

The result will be one of the SDK's standard BroadcastResponse or BroadcastFailure types, indicating the status of your transaction.

HD Wallets

For a long time, BIP32 was the standard way to structure a Bitcoin wallet. While type-42 has since taken over as the standard approach due to its increased privacy and open-ended invoice numbering scheme, it's sometimes still necessary to interact with legacy systems using BIP32 key derivation.

This guide will show you how to generate keys, derive child keys, and convert them to WIF and Bitcoin address formats. At the end, we'll compare BIP32 to the type-42 system and encourage you to adopt the new approach to key management.

Generating BIP32 keys

You can generate a BIP32 seed with the SDK as follows:

from bsv.hd import (
    mnemonic_from_entropy, seed_from_mnemonic, master_xprv_from_seed, Xprv, derive_xprvs_from_mnemonic
)

entropy = 'cd9b819d9c62f0027116c1849e7d497f'  # Or use some randomly generated string...

# snow swing guess decide congress abuse session subway loyal view false zebra
mnemonic: str = mnemonic_from_entropy(entropy)
print(mnemonic)

You can also import an existing key as follows:

seed: bytes = seed_from_mnemonic(mnemonic)
print(seed.hex())

master_xprv: Xprv = master_xprv_from_seed(seed)
print(master_xprv)

Now that you've generated or imported your key, you're ready to derive child keys.

Deriving Child Keys

BIP32 child keys can be derived from a key using the derive_xprv_from_mnemonic function. Here's a full example:

keys: List[Xprv] = derive_xprvs_from_mnemonic(mnemonic, path="m/44'/0'/0'", change=1, index_start=0, index_end=5)
for key in keys:
    # XPriv to WIF
    print(key.private_key().wif())

    key_xpub = key.xpub()

Any of the standard derivation paths can be passed into the derivation function.

Converting Between Formats

XPRIV keys can be converted to normal PrivateKey instances, and from there to WIF keys. XPUB keys can be converted to normal PublicKey instances, and from there to Bitcoin addresses. XPRIV keys can also be converted to XPUB keys:

# XPriv to WIF
print(key.private_key().wif())

# XPriv to XPub
key_xpub = key.xpub()

# XPub to public key
print(key_xpub.public_key().hex())

# XPub to address
print(key_xpub.public_key().address(), '\n')

This guide has demonstrated how to use BIP32 for key derivation and format conversion. You can continue to use BIP32 within BSV wallet applications, but it's important to consider the disadvantages and risks of continued use, which are discussed below.

Disadvantages and Risks

BIP32 allows anyone to derive child keys if they know an XPUB. The number of child keys per parent is limited to 2^31, and there's no support for custom invoice numbering schemes that can be used when deriving a child, only a simple integer. Finally, BIP32 has no support for private derivation, where two parties share a common key universe no one else can link to them, even while knowing the master public key. It's for these reasons that we recommend the use of type-42 over BIP32. You can read an equivalent guide here.

Linked Keys

Welcome to this type-42 key derivation guide! We're glad you're here, especially if you're migrating from an older key derivation system. Type-42 is more private, more elegant and it's easy to understand.

This guide will walk you through using type-42 keys in the context of a Bitcoin wallet.

Type 42 Key Derivation

In type-42 systems, you provide a counterparty key when deriving, as well as your own. There is always one public key and one private key. It's either:

  • Your private key and the public key of your counterparty are used to derive one of your private keys, or

  • Your private key and the public key of your counterparty are used to derive one of their public keys

When you and your counterparty use the same invoice number to derive keys, the public key you derive for them will correspond to the private key they derive for themselves. A private key that you derive for yourself will correspond to the public key they derived for you.

Once you understand those concepts, we're ready to jump into some code!

Alice and Bob

Let's consider the scenario of Alice and Bob, who want to exchange some Bitcoin. How can Alice send Bitcoins to Bob?

  1. Alice learns Bob's master public key, and they agree on the Bitcoin aount to exchange.

  2. They also agree on an invoice number.

  3. Alice uses Bob's master public key with her private key to derive the payment key she will use.

  4. Alice creates a Bitcoin transaction and pays Bob the money.

  5. Bob uses Alice's public key and his own private key to derive the corresponding private key, verifying it matches the transaction Alice sent him.

Here's an example:

# Master private keys:
alice = PrivateKey()
bob = PrivateKey()

# Master public keys:
alice_pub = alice.public_key()
bob_pub = bob.public_key()

# To pay Alice, they agree on an invoice number and then Bob derives a key where he can pay Alice.
payment_key = alice_pub.derive_child(bob, 'AMZN-44-1191213')

# The key can be converted to an address if desired...
print(payment_key.address())

# To unlock the coins, Alice derives the private key with the same invoice number, using Bob's public key.
payment_priv = alice.derive_child(bob_pub, 'AMZN-44-1191213')

# The key can be converted to WIF if desired...
print(payment_priv.wif())

# To check, Alice can convert the private key back into an address.
assert(payment_priv.public_key().address() == payment_key.address())

This provides privacy for Alice and Bob, even if eeryone in the world knows Alice and Bob's master public keys.

Going Further: Public Derivation

Sometimes, there is a legitimate reason to do "public key derivation" from a key, so that anyone can link a master key to a child key, like in BIP32. To accomplish this, rather than creating a new algorithm, we just use a private key that everyone already knows: the number 1.

print('Public keys:')
print(alice_pub.derive_child(PrivateKey(1), '1').hex())
print(alice_pub.derive_child(PrivateKey(1), '2').hex())
print(alice_pub.derive_child(PrivateKey(1), 'Bitcoin SV').hex())
print(alice_pub.derive_child(PrivateKey(1), '2-tempo-1').hex())

Because everyone knows the number 1, everyone can derive Alice's public keys with these invoice numbers. But only Alice can derive the corresponding private keys:

print('Private keys:')
print(alice.derive_child(PrivateKey(1).public_key(), '1').hex())
print(alice.derive_child(PrivateKey(1).public_key(), '2').hex())
print(alice.derive_child(PrivateKey(1).public_key(), 'Bitcoin SV').hex())
print(alice.derive_child(PrivateKey(1).public_key(), '2-tempo-1').hex())

The type-42 system enables both public and private key derivation, all while providing a more flexible and open-ended invoice numbering scheme than BIP32.

Fees

Bitcoin miners accept transactions into a block when they pay an appropriate fee. The transaction fee is simply the difference between the amounts used as input, and the amounts claimed by transaction outputs. This is to say, any amount of Bitcoins that are unclaimed (left over) after all transaction outputs have been fulfilled is given to the miner who solves the block in which the transaction is included.

To date, fees have generally been measured in satoshis per kilobyte of block space used by the transaction. However, the SDK allows you to create custom fee models that take other factors into account. This guide will show you the default fee model, and discuss how it might be customized in the future. Note that you'll need to consult with various miners if considering an alternative fee model, to make sure your transactions would still be included in the blockchain.

Default Fee Model

The .fee() method on a Transaction object takes a fee model as an optional parameter. The function of a fee model is to compute the number of satoshis that the transaction should pay in fees. Here's the interface all fee models need to follow:

class FeeModel(ABC):
    """
    Represents the interface for a transaction fee model.
    This interface defines a standard method for computing a fee when given a transaction.

    @interface
    @property {function} compute_fee - A function that takes a Transaction object and returns an integer representing the number of satoshis the transaction should cost.
    """
    
    @abstractmethod
    def compute_fee(self, transaction) -> int:
        pass

In short, a fee model is an object with a compute_fee function that, when called with a Transaction as its first and only parameter, will return the number of satoshis.

The default fee model, used if no other model is provided, looks like this:

class SatoshisPerKilobyte(FeeModel):
    """
    Represents the "satoshis per kilobyte" transaction fee model.
    """
    
    def __init__(self, value: int):
        """
        Constructs an instance of the sat/kb fee model.
        
        :param value: The number of satoshis per kilobyte to charge as a fee.
        """
        self.value = value

    def compute_fee(self, tx) -> int:
        """
        Computes the fee for a given transaction.
        
        :param tx: The transaction for which a fee is to be computed.
        :returns: The fee in satoshis for the transaction.
        """
        def get_varint_size(i: int) -> int:
            if i > 2 ** 32:
                return 9
            elif i > 2 ** 16:
                return 5
            elif i > 253:
                return 3
            else:
                return 1

        # Compute the (potentially estimated) size of the transaction
        size = 4  # version
        size += get_varint_size(len(tx.inputs))  # number of inputs

        for tx_input in tx.inputs:
            size += 40  # txid, output index, sequence number
            if tx_input.unlocking_script:
                script_length = len(tx_input.unlocking_script.serialize())
            elif tx_input.unlocking_script_template:
                script_length = tx_input.unlocking_script_template.estimated_unlocking_byte_length()
            else:
                raise ValueError('All inputs must have an unlocking script or an unlocking script template for sat/kb fee computation.')
            size += get_varint_size(script_length)  # unlocking script length
            size += script_length  # unlocking script

        size += get_varint_size(len(tx.outputs))  # number of outputs

        for tx_output in tx.outputs:
            size += 8  # satoshis
            length = len(tx_output.locking_script.serialize())
            size += get_varint_size(length)  # script length
            size += length  # script

        size += 4  # lock time

        # We'll use math.ceil to ensure the miners get the extra satoshi.
        fee = math.ceil((size / 1000) * self.value)
        return fee

Here, you can see we're computing the size of the transaction in bytes, then computing the number of satoshis based on the number of kilobytes.

Making Adjustments

Let's modify our fee model to check for a few custom cases, just as a purely theoretical example:

  • If the version of the transaction is 3301, the transaction is free.

  • If there are more than 3x as many inputs as there are outputs (the transaction is helping shrink the number of UTXOs), the transaction gets a 20% discount.

  • If there are more than 5x as many outputs as there are inputs, the transaction is 10% more expensive.

  • Other than that, the rules are the same as the Satoshis per Kilobyte fee model.

With these rules in place, let's build a custom fee model!

class ExampleFeeModel(FeeModel):
    """
    Represents the "satoshis per kilobyte" transaction fee model.
    Additionally, if the transactions version number is equal to 3301,
    then no fees are payed to the miner.
    """

    def __init__(self, value: int):
        self.value = value

    def compute_fee(self, tx) -> int:
        """
        Computes the fee for a given transaction.

        :param tx: The transaction for which a fee is to be computed.
        :returns: The fee in satoshis for the transaction.
        """

        def get_varint_size(i: int) -> int:
            if i > 2**32:
                return 9
            elif i > 2**16:
                return 5
            elif i > 253:
                return 3
            else:
                return 1

        # Version 3301 transactions are free :)
        if tx.version == 3301:
            return 0

        # Compute the (potentially estimated) size of the transaction
        size = 4  # version
        size += get_varint_size(len(tx.inputs))  # number of inputs

        for tx_input in tx.inputs:
            size += 40  # txid, output index, sequence number
            if tx_input.unlocking_script:
                script_length = len(tx_input.unlocking_script.serialize())
            elif tx_input.unlocking_script_template:
                script_length = (
                    tx_input.unlocking_script_template.estimated_unlocking_byte_length()
                )
            else:
                raise ValueError(
                    "All inputs must have an unlocking script or an unlocking script template for sat/kb fee computation."
                )
            size += get_varint_size(script_length)  # unlocking script length
            size += script_length  # unlocking script

        size += get_varint_size(len(tx.outputs))  # number of outputs

        for tx_output in tx.outputs:
            size += 8  # satoshis
            length = len(tx_output.locking_script.serialize())
            size += get_varint_size(length)  # script length
            size += length  # script

        size += 4  # lock time

        # We'll use math.ceil to ensure the miners get the extra satoshi.
        fee = math.ceil((size / 1000) * self.value)
        return fee

Now. when you create a new transaction and call the .fee() method with this fee model, it will follow the rules we have set above!

Merkle Path Verification

When verifying BEEF structures, it's necessary to ensure that all transactions are well-anchored: this is to say, that they come from inputs in the honest chain. The SDK doesn't ship with a headers client, but this guide shows an example of how to use it with Pulse: a popular client suitable for a wide range of use-cases.

Pre-requisites

As stated in the README, you will need to be running a Pulse instance. Get it up and running, and configure a level of authentication appropriate for your use-case:

docker pull bsvb/block-headers-service
docker run bsvb/block-headers-service:latest

Building our Client

The SDK's ChainTracker interface defines the required structure for our implementation, as follows:

class ChainTracker(ABC):
    """
    The Chain Tracker is responsible for verifying the validity of a given Merkle root
    for a specific block height within the blockchain.

    Chain Trackers ensure the integrity of the blockchain by
    validating new headers against the chain's history. They use accumulated
    proof-of-work and protocol adherence as metrics to assess the legitimacy of blocks.
    """

    @abstractmethod
    async def is_valid_root_for_height(self, root: str, height: int) -> bool:
        """
        Verify the validity of a Merkle root for a given block height.

        :param root: The Merkle root to verify.
        :param height: The block height to verify against.
        :return: A boolean indicating if the Merkle root is valid for the specified block height.
        """
        pass

Given an array of merkle roots and corresponding block heights, we return a boolean indicating whether they're all valid.

We can plug in the Block Header Service API with appropriate HTTP handling logic as follows:

class WhatsOnChainTracker(ChainTracker):
    def __init__(
            self,
            network: str = "main",
            api_key: Optional[str] = None,
            http_client: Optional[HttpClient] = None,
    ):
        self.network = network
        self.URL = f"https://api.whatsonchain.com/v1/bsv/{network}"
        self.http_client = (
            http_client if http_client else default_http_client()
        )
        self.api_key = api_key

    async def is_valid_root_for_height(self, root: str, height: int) -> bool:
        request_options = {"method": "GET", "headers": self.get_headers()}

        response = await self.http_client.fetch(
            f"{self.URL}/block/{height}/header", request_options
        )
        if response.ok:
            merkleroot = response.json()["data"]["merkleroot"]
            return merkleroot == root
        elif response.status_code == 404:
            return False
        else:
            raise Exception(
                f"Failed to verify merkleroot for height {height} because of an error: {response.json()}"
            )

    def get_headers(self) -> Dict[str, str]:
        headers = {
            "Accept": "application/json",
        }
        if self.api_key:
            headers["Authorization"] = self.api_key
        return headers

Now, we can use our WhatsOnChainTracker as a ChainTracker when calling the Transaction object's .verify() method. You can see an example in the BEEF verification guide.

This provides the ability to ensure that a transaction is well-anchored.

ECIES

Electrum ECIES is a protocol for exchanging encrypted data between parties. It has been commonly used in many applications, and while the SDK's native Message Encryption functionality is the preferred approach for new applications (due to its use of GCM over CBC and aditional layers of security described below), legacy systems still use ECIES and this guide will demonstrate how it can be done.

Message Encryption

In ECIES, a message can be encrypted directly to the public key of the recipient, either from your private key or from a random private key. The public key can either be included or excluded from the message. Check out the below examples:

from bsv import PrivateKey

private_key = PrivateKey('L5agPjZKceSTkhqZF2dmFptT5LFrbr6ZGPvP7u4A6dvhTrr71WZ9')
public_key = private_key.public_key()

plain = 'hello world'

# use public key to encrypt
encrypted = public_key.encrypt_text(plain)
print(encrypted)

# decrypt with the corresponding private key
print(private_key.decrypt_text(encrypted))

Considerations

This guide has shown how to use Electrum ECIES encryption. While this approach has been used by many legacy systems, the SDK's native encryption has the following benefits:

  • Additional Security Layer: The native SDK implentation, based on BRC-78, employs an additional layer of security by utilizing a one-off ephemeral key for the encryption process. Even if the key for a particular message is discovered, it does not compromise the private keys of either of the parties. Different keys are used for every message, adding an additional step for attackers.

  • Incompatibility with BRC-43 Invoice Numbers: The native approach is fully compatible with BRC-43 invoice numbers, and the BRC-2 encryption process, making it possible for users of the BRC-56 standard wallet able to natively use the system under their MetaNet identities. ECIES is not compatible with these standards.

  • Use of GCM over CBC: While this is not a security risk, GCM supports range-based encryption and decryption. This may make it better than CBC if you need to send parts of a large encrypted dataset over the network.

Despite these drawbacks, Electrum ECIES still remains a fundamentally secure and robust encryption scheme.