Created
January 2, 2020 12:56
-
-
Save sondreb/7a6697b93e83a37b8db857685d3215d4 to your computer and use it in GitHub Desktop.
A quick coding dojo teaching some basics of Bitcoin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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