I'm Dan Gould

Multisignature Bitcoin in C#

Bitcoin allows shared ownership of coins by using multisignature transactions.

This guide walks thorugh:

  • writing a multisignature transaction by hand
  • broadcasting it to blockchain
  • the practical applications of such a transaction


Huge thanks to @NicolasDorier, @nopara73, and everyone else who contributed to Programming the Blockchain in C#. Much of this workshop draws from that text and the NBitcoin library Nicolas wrote which it details.

Thanks also to Stratis for the generous donation to BostonHacks 2018 which made this originally possible.

Let’s get started

  1. Install .NET Core as documented here.
  2. Install VSCode, or Visual Studio, or your favorite text editor.
  3. Install the C# Extension or analagous plugin for your editor like omnisharp for sublime. Without one making mistakes will be too easy.

I highly recommend you type all of the commands and code rather than copy and paste. You’ll more quickly get a feel for the library and be able to figure out where your mistakes are.

Create a new project

Enter the following commands in your command line:

mkdir StratisProject
cd StratisProject
dotnet new console
dotnet add package NBitcoin
dotnet restore

Then edit Program.cs:

// Program.cs

using System;
using NBitcoin;

namespace StratisProject
    class Program
        static void Main(string[] args)
	    Network network = Network.TestNet;
	    var treasurer = new Key();

	    // Fun Fact: WIF is Wallet Imput Format - a base58 representation of your key
            Console.WriteLine("treasurer key: " +  treasurer.GetWif(network));

            var alice = new Key();
            var bob = new Key();

            Console.WriteLine("Alice     key: " + alice.GetWif(network));
            Console.WriteLine("Bob       key: " + bob.GetWif(network));

Run it in console:

dotnet run

This program generates three bitcoin secret keys (sk). Open a new file and write them down. We’ll need them in the next steps.

// Keys.txt

// Just example keys. Yours will of course be actual base58 encoded WIF keys.
treasurer key: <cSMW1AvuqDX5NG3Gy4ktxx1yBWEKGU5r3p6NbXuCK6LkjtH12FLe>
Alice     key: <cSWyMTFYWBVjv7tzvpj1rQgMUwccs5yGgi9ZiHzP978nohWEDH9w>
Bob       key: <cUWedW9rd7HnsTdks1HDwUTTWRDsMvHz2kY6dbv5pH4jgHU1pN9H>

Substitute your keys for the ones above. Replace your existing Main method with the following:

// Program.cs (Main method snipet)

static void Main(string[] args)
    Network network = Network.TestNet;
    // Replace the constructor argument with your own noted secrets from the last step
    var treasurer = new BitcoinSecret("cPaLw36GPtbfiq5rrEWsQLFn1oatdDmj8VRonnveEbFDctVAg5iy");
    var alice = new BitcoinSecret("cPaLw36GPtbfiq5rrEWsQLFn1oatdDmj8VRonnveEbFDctVAg5iy");
    var bob = new BitcoinSecret("cPaLw36GPtbfiq5rrEWsQLFn1oatdDmj8VRonnveEbFDctVAg5iy");

    Console.WriteLine("treasurer key: " + treasurer.PrivateKey.GetWif(network));
    Console.WriteLine("Alice     key: " + alice.PrivateKey.GetWif(network));
    Console.WriteLine("Bob       key: " + bob.PrivateKey.GetWif(network));
dotnet run

❓ When you run this program, is the WalletInputFormat sk logged the same? Why or why not?

Writing a Multisig Transaction

In order to demonstrate this ownership we will create a ScriptPubKey that represents an m-of-n multisig. In order to spend the coins this ScriptPubKey owns, m private keys out of the n given public keys provided must sign the spending transaction.

Let’s create a multisig contract where two of the three Bob, Alice and the treasurer need to sign a transaction in order to spend a coin.

// Program.cs
// Continued Main method:

var scriptPubKey = PayToMultiSigTemplate
    .GenerateScriptPubKey(2, new[] { bob.PubKey, alice.PubKey, treasurer.PubKey });

Console.WriteLine("PubKey script: " + scriptPubKey);

Run it:

dotnet run

The program now generates this script which you can use as a public key (coin destination address):

2 0282213c7172e9dff8a852b436a957c1f55aa1a947f2571585870bfb12c0c15d61 036e9f73ca6929dec6926d8e319506cc4370914cd13d300e83fd9c3dfca3970efb 0324b9185ec3db2f209b620657ce0e9a792472d89911e0ac3fc1e5b5fc2ca7683d 3 OP_CHECKMULTISIG

As you can see, the scriptPubkey has the following form: <sigsRequired> <pubKeys…> <pubKeysCount> OP_CHECKMULTISIG

scriptPubKey is a Tx output which specifies conditions that must be satisfied in order to spend the value of the output. In this multi-sig transaction, we require 2-of-3 specified pubkeys to sign as condition to spend.

Notable Example:

The cold storage wallet of the Bitfinex exchange is a single 3-of-6 multisig address 3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r from which (as of November 2018) over ₿1,943,295 ≈ $12.3 billion has flowed. Presumably, the keys are kept very safe by Bitfinex’s operators.

P2SH 2-of-3 Transaction

This PubKey Script (scriptPubKey) we logged, though valid, doesn’t look very much like a wallet-friendly address. We will run it through a function so a sender can use it like any other address.

// Program.cs (cont.)

var redeemScript = PayToMultiSigTemplate
    .GenerateScriptPubKey(2, new[] { bob.PubKey, alice.PubKey, treasurer.PubKey })

Console.WriteLine("redeemScript: "+ redeemScript);

Run it:

dotnet run

And view the new output.

OP_HASH160 57b4162e00341af0ffc5d5fab468d738b3234190 OP_EQUAL

This output hash represents the hash of the previous multisig script containing OP_CHECKMULTISIG

Since it’s a hash, we can easily convert it to a base58 bitcoin address with the following snippet:

// Program.cs (cont.)

Console.WriteLine("multi-sig address:" + redeemScript.Hash.GetAddress(network));

Note: Details are important. We must pay to the redeemScript.Hash not the redeemScript. We want P2SH not P2PK[H].

Excellent! Now we can load it up the same we would our FullNode wallet.

Give the script value 🤑

The following parts of this guide include live transactions on the bitcoin network. If you send funds to the wrong place, they’re gone forever. ==Don’t run the program unless you understand what it’s doing. Fret not; ask for help.

dotnet run

Copy down your new redeemScript.Hash address. Enter it into this bitcoin faucet to get free coin for testing. If that’s down, here’s the backup faucet.

Search the same receive address (redeemScript.Hash address) or transactionId (txId) on a block explorer to view the tx network status.

We’re going to need some network connectivity to get funds out from now cold storage.

Become a node (or a leech)

Who runs a bitcoin node? Send from your own node.

Add QBitNinja.Client to your project:

dotnet add package QBitNinja.Client

reference this nifty node-as-a-service at the top of Program.cs

// Program.cs
// ... after the other `using` statements

using QBitNinja.Client;

// ...
// Append to your `Program.cs` Main method
// ...

var client = new QBitNinjaClient(network);

// Replace "0acb..." with the txId from the block explorer from the faucet
// If the faucet doesn't work, look up the redeemScript.Hash address on the explorer
// You will find a txId related to that address. If no tx appears, try another faucet.

var receiveTransactionId = uint256.Parse("0acb6e97b228b838049ffbd528571c5e3edd003f0ca8ef61940166dc3081b78a");
var receiveTransactionResponse = client.GetTransaction(receiveTransactionId).Result;

// if this fails, it's ok. It hasn't been confirmed in a block yet. Proceed

Make a Payment 💸

Because our funds are locked in a 2-of-3 contract spending is a bit more complicated than just calling Transaction.Sign

Later we will explain what is going on under the hood. For now let’s use the TransactionBuilder to sign the transaction.

From Where?

Let’s see which output of our transaction we can spend. ❔: What does it mean to “spend” cryptocurrency?

// Program.cs (cont.)

var receivedCoins = receiveTransactionResponse.ReceivedCoins;
OutPoint outpointToSpend = null;
ScriptCoin coinToSpend = null;
foreach (var c in receivedCoins)
        // If we can make a ScriptCoin out of our redeemScript
        // we "own" this outpoint
        ScriptCoin coinToSpend = new ScriptCoin(c, redeemScript);
        outpointToSpend = c.Outpoint;
    catch {}
if (outpointToSpend == null)
	throw new Exception("TxOut doesn't contain any our ScriptPubKey");
Console.WriteLine("We want to spend outpoint #{0}", outpointToSpend.N + 1);

To Who?

We already know Lucas’s address is mv4rnyY3Su5gjcDNzbMLKBQkBicCtHUtFB

// Program.cs (cont.)

var lucasAddress = BitcoinAddress.Create("mv4rnyY3Su5gjcDNzbMLKBQkBicCtHUtFB", network);

‼️: We could sign & broadcast this transaction now. What’s missing? What problem might we have?

Typically we’d add a change output but lucas deserves all our bit wealth

Sign the Contract 🖋️

We need 2-of-3. Even if the treasurer doesn’t approve, Alice & Bob’ll have their way.

// Program.cs (cont.)

TransactionBuilder builder = network.CreateTransactionBuilder();
var minerFee = new Money(0.0002m, MoneyUnit.BTC);
var sendAmount = txInAmount - minerFee;
var txInAmount = (Money)receivedCoins[(int)outpointToSpend.N].Amount;

In practice, we would use a FullNode to estimate the miner fee. Since we’re rich we don’t care.

// Program.cs (cont.)

Transaction unsigned =
	.Send(lucasAddress, sendAmount)
	.BuildTransaction(sign: false);

// Alice signs it
Transaction aliceSigned = 

// Program.cs (cont.)

// Gotta get Bob's approval too
Transaction bobSigned =

Now, Bob and Alice can combine their signatures into one transaction. This transaction will then be valid, because two signatures were used from the three (Bob, Alice and Satoshi) original signatures that were initially provided. The requirements of the ‘two-of-three’ multisig have therefore been met. If this wasn’t the case, the network would not accept this transaction, because the nodes reject all transactions without complete signatures.

// Program.cs (cont.)

Transaction fullySigned =
        .CombineSignatures(aliceSigned, bobSigned);


Run it:

dotnet run


  "in": [
      "prev_out": {
        "hash": "9df1e011984305b78210229a86b6ade9546dc69c4d25a6bee472ee7d62ea3c16",
        "n": 0
      "scriptSig": "0 3045022100a14d47c762fe7c04b4382f736c5de0b038b8de92649987bc59bca83ea307b1a202203e38dcc9b0b7f0556a5138fd316cd28639243f05f5ca1afc254b883482ddb91f01 3044022044c9f6818078887587cac126c3c2047b6e5425758e67df64e8d682dfbe373a2902204ae7fda6ada9b7a11c4e362a0389b1bf90abc1f3488fe21041a4f7f14f1d856201"
  "out": [
      "value": "1.00000000",
      "scriptPubKey": "OP_DUP OP_HASH160 d4a0f6c5b4bcbf2f5830eabed3daa7304fb794d6 OP_EQUALVERIFY OP_CHECKSIG"

The transaction is now ready to be sent to the network, but notice that the CombineSignatures() method was critical here, because both the aliceSigned and the bobSigned transactions were only partially signed, therefore not acceptable by the network. CombineSignatures() combined the two partially signed transactions into one fully signed transaction.

Broadcast to the Network

Finally, let’s send it to bitcoin nodes and get it in the blockchain.

// Program.cs (cont.)

var broadcastResponse = client.Broadcast(fullySigned).Result;
if (!broadcastResponse.Success)
    Console.Error.WriteLine("ErrorCode: " + broadcastResponse.Error.ErrorCode);
    Console.Error.WriteLine("Error message: " + broadcastResponse.Error.Reason);
    Console.WriteLine("Success! You can check out the hash of the transaciton in any block explorer:");
dotnet run

Congrats! Bask in your newfound glory 🥳🎉. You just deployed raw bitcoin Script to the blockchain. It should be there forever.