Skip to content

Instantly share code, notes, and snippets.

@sondreb
Created January 2, 2020 12:56
Show Gist options
  • Save sondreb/7a6697b93e83a37b8db857685d3215d4 to your computer and use it in GitHub Desktop.
Save sondreb/7a6697b93e83a37b8db857685d3215d4 to your computer and use it in GitHub Desktop.
A quick coding dojo teaching some basics of Bitcoin
using NBitcoin;
using Newtonsoft.Json;
using Xunit;
namespace BitcoinDojo
{
public class Tests
{
[Fact]
public void BasicIdentityAndEncryptionUsingBitcoin()
{
var recoveryPhrase = "obtain design mistake life call inside smooth gloom sunset bless winter tenant";
// TODO: Create a network that uses definition not used in BIP44. Perhaps the derivation path should have different top-level
// than the crypto currencies in the BIP44 slip list.
var network = NBitcoin.Network.Main;
var mnemonic = new Mnemonic(recoveryPhrase, Wordlist.English);
// masterNode
var masterNode = mnemonic.DeriveExtKey();
// Extended Public Key
var extPubKey = masterNode.Neuter();
// Private key in WIF format.
var xprv = masterNode.GetWif(network);
Assert.Equal("xprv9s21ZrQH143K3uMUk5XNDqRTiuqUYYuUYtvzqynVfxNYyzeHW75QoH5d1xup2osukC2G4MXCUL8gEKXZpXSEbRwJvzPLPAsbuzgw5vSwGsA", xprv.ToString());
// Get the public key in WIF format.
var xpub = extPubKey.GetWif(network);
Assert.Equal("xpub661MyMwAqRbcGPRwr74NayNCGwfxx1dKv7rbeNC7EHuXrnyS3ePfM5Q6sHLdAzttHBeHcJRCc698DukPc7VX7a163PkSyKQWn9JuMHntLjA", xpub.ToString());
// Get the very first Bitcoin address from the account 0 for this master node.
var account0 = masterNode.Derive(new KeyPath("m/44'/0'"));
var account0Address0 = account0.Derive(0).Derive(0);
// This is the Bitcoin address we want to receive payment on.
var account0Address0Text = account0Address0.GetPublicKey().GetAddress(ScriptPubKeyType.Legacy, NBitcoin.Network.Main).ToString();
Assert.Equal("12nzc2rcabyabz16HCNhSgK6G2obEhhZAo", account0Address0Text);
// Create some identities based on a different purpose (302) value:
var identity0 = masterNode.Derive(new KeyPath("m/302'/0'"));
var identity1 = masterNode.Derive(new KeyPath("m/302'/1'"));
var identity2 = masterNode.Derive(new KeyPath("m/302'/2'"));
var identity0Address1 = identity0.Derive(new KeyPath("0/0")); // "m/302'/0'/0/0"
var identity0Address2 = identity0.Derive(new KeyPath("0/1"));
var identity0Address3 = identity0.Derive(new KeyPath("0/2"));
var identity1Address1 = identity1.Derive(new KeyPath("0/0")); // "m/302'/1'/0/0"
var identity1Address2 = identity1.Derive(new KeyPath("0/1"));
var identity1Address3 = identity1.Derive(new KeyPath("0/2"));
var identity2Address1 = identity2.Derive(new KeyPath("0/0")); // "m/302'/2'/0/0"
var identity2Address2 = identity2.Derive(new KeyPath("0/1"));
var identity2Address3 = identity2.Derive(new KeyPath("0/2"));
var address1 = identity0Address1.PrivateKey.PubKey.GetAddress(ScriptPubKeyType.Segwit, network).ToString();
Assert.Equal("bc1qnef2c083jcsq5eyj4n0df7qr3ffkeyss8mwxa0", address1); // Segwith address type
var address2 = identity1Address1.PrivateKey.PubKey.GetAddress(ScriptPubKeyType.SegwitP2SH, network).ToString();
Assert.Equal("3AdmxvSyn5FA3Ghz85g9HuHsMZiKGsG3jG", address2); // Segwith Pay To Script Hash address type
var address3 = identity2Address1.PrivateKey.PubKey.GetAddress(ScriptPubKeyType.Legacy, network).ToString();
Assert.Equal("1DgdPY3y3S8Q3ZuFXhJ4QfFkcTuZLrYysF", address3); // Legacy address type, Pay to Public Key Hash.
var identity0PublicName = identity0Address1.GetPublicKey();
// This is what we'll use to show in the UI to users, more user-friendly than other values.
Assert.Equal("9e52ac3c", identity0PublicName.GetHDFingerPrint().ToString());
// Or should we simply rely on a custom address format (custom network class)?
var identityPublicKeyHash = identity0PublicName.Hash.ToString();
// This is the full public key hash, which is what we'll use to link.
Assert.Equal("9e52ac3cf196200a6492acded4f8038a536c9210", identityPublicKeyHash);
// Perhaps this could be an URI format?
var identityAddress = "id:9e52ac3cf196200a6492acded4f8038a536c9210"; // or "identification:"?
// We can at least not use the pubkeyhash (Hash160) directly, as the NBitcoin APIs expect address. So override...
identityPublicKeyHash = identity0PublicName.GetAddress(ScriptPubKeyType.Legacy, NBitcoin.Network.Main).ToString();
// Now beforehand, the IDENTITY0 will need to get a copy of the PUBLIC KEY of IDENTITY1.
// To avoid too much duplicate code, we'll just reuse the same master node for this.
var receiverPublicKeyHash = identity1Address1.GetPublicKey().ToHex();
// Create the connection request and sign it your own private key.
// SENDER PUBLIC KEY | BITCOIN ADDRESS | RECEIVER PUB KEY
var msgToSign = $"{identityPublicKeyHash}|{account0Address0Text}|{receiverPublicKeyHash}";
// In reality, we'll sign 100% of what is sent over the wire, except the signature.
var signature = identity0Address1.PrivateKey.SignMessage(msgToSign);
dynamic request = new
{
network = "main",
origin = identityPublicKeyHash,
target = receiverPublicKeyHash,
address = account0Address0Text,
signature = signature
};
// We'll use MessagePack in real-life.
var json = JsonConvert.SerializeObject(request);
// Restore the public key from the shared value.
var publicKey = new PubKey(receiverPublicKeyHash);
// Encrypt the request with the public key of the receiver.
var cipher = publicKey.Encrypt(json);
// Sender the cipher from IDENTIY0 to IDENTITY1!
var decryptedMessage = identity1Address1.PrivateKey.Decrypt(cipher);
dynamic receivedRequest = JsonConvert.DeserializeObject(decryptedMessage);
// After the hub receives the request, they must verify that it has not been tampered with:
BitcoinPubKeyAddress publicAddress = new BitcoinPubKeyAddress((string)receivedRequest.origin, Network.Main);
// Re-create the message we must validate the signature against. Here we'll actually read our own public key (hex) from our local instance and not from
// the message itself, just to ensure that the incoming message is targeted us and not just the one specified in the payload.
var verificationMessage = $"{receivedRequest.origin}|{receivedRequest.address}|{identity1Address1.GetPublicKey().ToHex()}";
// Verify the signature, that means the origin, address and receiver has not been tampered with.
bool isSignatureValid = publicAddress.VerifyMessage(verificationMessage, (string)receivedRequest.signature);
Assert.True(isSignatureValid);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment