Skip to content

Instantly share code, notes, and snippets.

@jspilman
Last active August 19, 2017 12:30
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jspilman/8396495 to your computer and use it in GitHub Desktop.
Save jspilman/8396495 to your computer and use it in GitHub Desktop.
Working implementation of stealth payments in Bitcoin using OP_RETURN! See TxID: 6e8576d6f65947b249a19402b6359c5a490abf67d50869a18518ccae41f94419 on Test NeT
// BIP32 Wallet
byte[] entropy = Util.DoubleSHA256(Encoding.ASCII.GetBytes("Stealth Address"));
HdNode wallet = HdNode.Create(entropy, mainNet: false).GetSecretChild(0);
// A TxID on Test-Net with 1BTC in Vout[1], spendable by wallet/0'/0'
byte[] unspentPubKey = wallet.GetSecretChild(0, 0).PublicKey;
byte[] unspentPrivKey = wallet.GetSecretChild(0, 0).PrivateKey;
string unspentAddr = Util.PubKeyToAddress(unspentPubKey, mainNet: false);
byte[] unspentTxId = Util.HexToBytes("4b8fd9c4f5cb233c687e3e883f7c284f9abc2698dd08f1ec6770f488a27a9704");
TxOut unspentTxOut = TxOut.PayToPubKeyHash(Util.Amount("1"), unspentPubKey);
Array.Reverse(unspentTxId);
// Two static public keys -- wallet/1'/0', and wallet/1'/1'
byte[] d1 = wallet.GetSecretChild(1, 0).PrivateKey;
byte[] d2 = wallet.GetSecretChild(1, 1).PrivateKey;
byte[] Q1 = wallet.GetSecretChild(1, 0).PublicKey;
byte[] Q2 = wallet.GetSecretChild(1, 1).PublicKey;
Console.WriteLine("Q1: " + Util.BytesToHex(Q1));
Console.WriteLine("Q2: " + Util.BytesToHex(Q2));
Console.WriteLine();
// Generate an Ephemeral private and public key
// NOTE: Not quite randomly generated for now, to make it easier to reproduce this
//byte[] e = EC.NewPrivateKey();
//byte[] P = EC.GetPublicKey(e, compressed: true);
byte[] e = wallet.GetSecretChild(1, 2).PrivateKey;
byte[] P = wallet.GetSecretChild(1, 2).PublicKey;
Console.WriteLine("e: " + Util.BytesToHex(e));
Console.WriteLine("P: " + Util.BytesToHex(P));
Console.WriteLine();
// Calculated shared secrets with Q2 (from Payee perspective)
byte[] S = EC.DH(e, Q2);
Console.WriteLine("S: " + Util.BytesToHex(S));
Console.WriteLine();
// Recalculate shared secrets with d2 (from Payer perspective)
byte[] PayeeS = EC.DH(d2, P);
CollectionAssert.AreEqual(S, PayeeS);
// Calculate stealth/derived public keys from Q1 -> Q1' and Q2 -> Q2'
// Q1' = Q + (SHA256(S || 1) * G)
// Q2' = Q + (SHA256(S || 2) * G)
var key1 = Util.SingleSHA256(S.Concat(new byte[] { 1 }));
byte[] q1New = EC.PointAdd(Q1, key1);
Console.WriteLine("Key 1: " + Util.BytesToHex(key1));
Console.WriteLine("Q1': " + Util.BytesToHex(q1New));
Console.WriteLine();
var key2 = Util.SingleSHA256(S.Concat(new byte[] { 2 }));
byte[] q2New = EC.PointAdd(Q2, key2);
Console.WriteLine("Key 2: " + Util.BytesToHex(key2));
Console.WriteLine("Q2': " + Util.BytesToHex(q2New));
Console.WriteLine();
// Prep the unspent as a TxIn
Script unspentScriptPubKey = unspentTxOut.ScriptPubKey;
TxIn userUnspentTxIn = new TxIn(unspentTxId, nIn: 1);
// Build the transaction
Transaction stealthTx = new Transaction();
stealthTx.Vin.Add(userUnspentTxIn);
stealthTx.Vout.Add(TxOut.PayToMultiSig(Util.Amount(".995"), 2, 2, q1New, q2New));
stealthTx.Vout.Add(TxOut.OpReturn(P));
// Sign the transaction -- ScriptSig for Vin[0] will be <Sig> <PubKey>
stealthTx.Sign(unspentPrivKey, unspentScriptPubKey, 0, SigHash.All);
stealthTx.Vin[0].ScriptSig.Push(unspentPubKey);
// Verify TX is complete given the scriptPubKeys from the input transactions
Assert.IsTrue(stealthTx.Verify(unspentScriptPubKey));
Console.WriteLine("Stealth TX: " + Util.BytesToHex(stealthTx.Serialize()));
Console.WriteLine("Stealth TxID: " + stealthTx.TxIdStr); // reversed bytes to follow convention
Console.WriteLine();
// An alternative transaction construction...
stealthTx = new Transaction();
stealthTx.Vin.Add(userUnspentTxIn);
stealthTx.Vout.Add(TxOut.PayToPubKeyHash(Util.Amount(".995"), q1New));
stealthTx.Vout.Add(TxOut.OpReturn(P));
// Sign the transaction -- ScriptSig for Vin[0] will be <Sig> <PubKey>
stealthTx.Vin[0].ScriptSig = Script.Empty; // reset the signature which we added above
stealthTx.Sign(unspentPrivKey, unspentScriptPubKey, 0, SigHash.All);
stealthTx.Vin[0].ScriptSig.Push(unspentPubKey);
// Verify TX is complete given the scriptPubKeys from the input transactions
Assert.IsTrue(stealthTx.Verify(unspentScriptPubKey));
Console.WriteLine("Stealth TX: " + Util.BytesToHex(stealthTx.Serialize()));
Console.WriteLine("Stealth TxID: " + stealthTx.TxIdStr); // reversed bytes to follow convention
public static byte[] DH(byte[] privKey, byte[] pubKeyBytes)
{
var pubKey = new OpenSSL.Crypto.EC.Point(EcGroup, pubKeyBytes);
var result = pubKey.Multiply(OpenSSL.Core.BigNumber.FromArray(privKey), EcBnContext);
OpenSSL.Core.BigNumber x = new OpenSSL.Core.BigNumber();
OpenSSL.Core.BigNumber y = new OpenSSL.Core.BigNumber();
result.GetAffineCoordinatesGFp(x, y, EcBnContext);
byte[] xBytes = new byte[x.Bytes];
x.ToBytes(xBytes);
return xBytes;
}
Test Name: StealthPayment
Test FullName: BitcoinUnitTests.TransactionTests.StealthPayment
Test Source: c:\Source\BitPostage\BitPostage\BitcoinUnitTests\Transaction.cs : line 264
Test Outcome: Passed
Test Duration: 0:00:01.2505994
Result StandardOutput:
Q1: 025A7B91C4E08284DFA7701BCD2B0BBA4A755CB7BDFDB75308E48CD570BF917510
Q2: 03AEF1BD2BCFDAF5092CED87B0F19B95A604C0176ADF06542FCA4D2771FCF64118
e: 7B381283DDCBA6C447F3EE76716722F58C83E448AF26B14F10C6A064924413AD
P: 02FDF5C5E6B5D3419E42CC0E12A53B3BD42795A2BDD937739E5EB6ECA5CF542206
S: 51DD1F217272B5CB4542A62452AFAFDA3745EB573F27541402E45F9A3883F5EC
Key 1: B31BD7791E48B225D978B40A38EB1695CCA6A52EAF425339B6B30C9E02F1310A
Q1': 023CDFDF2DC653B5C33646CDFF36DBD899DEA37A6B431CD412631D5112507D1D5C
Key 2: B3A49A0C1C7A1B8AB8892E0A549E56BD8BD5BC38714DBA6A479EF3C2E42DAA8F
Q2': 03305DDDEC8FE97CBAF4A38BB41CB6AF6DD5F44E4B68EBD733F634230EB810731C
-- Note: The following TX is an example MULTISIG, I didn't actually put this one on the blockchain
Stealth TX: 010000000104977AA288F47067ECF108DD9826BC9A4F287C3F883E7E683C23CBF5C4D98F4B010000006A4730440220344AA64AE604E910B14BCD96BB653C6AB6EF70BF81B0C84D7F6B9EB445A0C8C002205A65D3AEF383E28741EA21F1299319B38CB87746D6CA2B8E26117EA205B849C4012102FD1B4F1954D282CF243576885CA28AF5CC15D5A66443226021FE5986BB58EDFBFFFFFFFF02E03FEE0500000000475221023CDFDF2DC653B5C33646CDFF36DBD899DEA37A6B431CD412631D5112507D1D5C2103305DDDEC8FE97CBAF4A38BB41CB6AF6DD5F44E4B68EBD733F634230EB810731C52AE0000000000000000246A4C2102FDF5C5E6B5D3419E42CC0E12A53B3BD42795A2BDD937739E5EB6ECA5CF54220600000000
Stealth TxID: 773e1623fd0ebcd3e1e66e0778ed7fba55519e0f8f0aa976775e6782bcaf25e0
-- NOTE: This following TX is an example pay-to-pubKey, this one IS actually on the block chain
-- The payment ends up being sent to address n3wq8rDH4nfqWhK6XmQnh9BupJ4mAZK2F5 which
-- is unlinkable to Q1 without having d2
Stealth TX: 010000000104977AA288F47067ECF108DD9826BC9A4F287C3F883E7E683C23CBF5C4D98F4B010000006B4830450221009E416387571B5C5A9307C34695B848A938BC35152C73D4FE8B33DD83FFB7865802203629F866A7A6A6516A49E9AA71B87A305C8E5F117C458F1AA2DE3AE038044EBA012102FD1B4F1954D282CF243576885CA28AF5CC15D5A66443226021FE5986BB58EDFBFFFFFFFF02E03FEE05000000001976A914F60730C4E1E0ACF6B7B5C2B6C4E801D18F1B291488AC0000000000000000246A4C2102FDF5C5E6B5D3419E42CC0E12A53B3BD42795A2BDD937739E5EB6ECA5CF54220600000000
Stealth TxID: 6e8576d6f65947b249a19402b6359c5a490abf67d50869a18518ccae41f94419
// BIP32 Wallet
byte[] entropy = Util.DoubleSHA256(Encoding.ASCII.GetBytes("Stealth Address"));
HdNode wallet = HdNode.Create(entropy, mainNet: false).GetSecretChild(0);
// Two static keys -- wallet/1'/0', and wallet/1'/1'
byte[] d1 = wallet.GetSecretChild(1, 0).PrivateKey;
byte[] d2 = wallet.GetSecretChild(1, 1).PrivateKey;
byte[] Q1 = wallet.GetSecretChild(1, 0).PublicKey;
byte[] Q2 = wallet.GetSecretChild(1, 1).PublicKey;
Console.WriteLine("Q1: " + Util.BytesToHex(Q1));
Console.WriteLine("Q2: " + Util.BytesToHex(Q2));
// Decode the transaction that we sent above... this is on the TestNet BlockChain
Transaction stealthTx = Transaction.Parse(Util.HexToBytes("010000000104977AA288F47067ECF108DD9826BC9A4F287C3F883E7E683C23CBF5C4D98F4B010000006B4830450221009E416387571B5C5A9307C34695B848A938BC35152C73D4FE8B33DD83FFB7865802203629F866A7A6A6516A49E9AA71B87A305C8E5F117C458F1AA2DE3AE038044EBA012102FD1B4F1954D282CF243576885CA28AF5CC15D5A66443226021FE5986BB58EDFBFFFFFFFF02E03FEE05000000001976A914F60730C4E1E0ACF6B7B5C2B6C4E801D18F1B291488AC0000000000000000246A4C2102FDF5C5E6B5D3419E42CC0E12A53B3BD42795A2BDD937739E5EB6ECA5CF54220600000000"));
Console.WriteLine("Stealth TxID: " + stealthTx.TxIdStr);
Console.WriteLine();
// Vout[1] has a ScriptPubKey of 'OP_RETURN PUSHDATA1 <DATA>'
byte[] TxP = stealthTx.Vout[1].ScriptPubKey.Ops[1].Data.ToArray();
Console.WriteLine("Stealth Tx Vout[1]: " + Util.BytesToHex(stealthTx.Vout[1].ScriptPubKey.Raw));
Console.WriteLine("Stealth Tx P: " + Util.BytesToHex(TxP));
Console.WriteLine();
// Recalculate shared secrets with d2 (using 'P' from the Tx)
byte[] S = EC.DH(d2, TxP);
Console.WriteLine("Shared Secret: " + Util.BytesToHex(S));
Console.WriteLine();
// First lets DETECT this payment as if we only had d2 and Q1 pubKey
var key = Util.SingleSHA256(S.Concat(new byte[] { 1 }));
byte[] q1New = EC.PointAdd(Q1, key);
Console.WriteLine("Key 1: " + Util.BytesToHex(key));
Console.WriteLine("Q1': " + Util.BytesToHex(q1New));
// We paid to HASH160 of Q1'
// StealthTX looks like: OP_DUP OP_HASH160 <Hash160(Q1')> OP_EQUALVERIFY OP_CHECKSIG
CollectionAssert.AreEqual(Util.Hash160(q1New), stealthTx.Vout[0].ScriptPubKey.Ops[2].Data.ToArray());
Console.WriteLine("Hash160(Q1'): " + Util.BytesToHex(Util.Hash160(q1New)));
Console.WriteLine();
// They matched! So a payment was detected using d2 and Q1, but we can't spend it without d1
// Now let's spend it...
byte[] d1New = EC.AddModN(d1, key);
// Spend the stealth payment with our derived private keys
Transaction spendStealth = new Transaction();
spendStealth.Vin.Add(new TxIn(stealthTx.TxID, 0));
spendStealth.Vout.Add(TxOut.PayToPubKeyHash(Util.Amount(".990"), wallet.GetSecretChild(0, 1).PublicKey));
// Sign the transaction -- ScriptSig for Vin[0] will be <sig> <pubKey>
spendStealth.Sign(d1New, stealthTx.Vout[0].ScriptPubKey, 0, SigHash.All);
spendStealth.Vin[0].ScriptSig.Push(q1New);
Assert.IsTrue(spendStealth.Verify(stealthTx.Vout[0].ScriptPubKey));
Console.WriteLine("Spend Stealth TX: " + Util.BytesToHex(spendStealth.Serialize()));
Console.WriteLine("Spend Stealth TxID: " + spendStealth.TxIdStr);
Test Name: SpendStealth
Test FullName: BitcoinUnitTests.TransactionTests.SpendStealth
Test Source: c:\Source\BitPostage\BitPostage\BitcoinUnitTests\Transaction.cs : line 362
Test Outcome: Passed
Test Duration: 0:00:01.2757872
Result StandardOutput:
Q1: 025A7B91C4E08284DFA7701BCD2B0BBA4A755CB7BDFDB75308E48CD570BF917510
Q2: 03AEF1BD2BCFDAF5092CED87B0F19B95A604C0176ADF06542FCA4D2771FCF64118
Stealth TxID: 6e8576d6f65947b249a19402b6359c5a490abf67d50869a18518ccae41f94419
Stealth Tx Vout[1]: 6A4C2102FDF5C5E6B5D3419E42CC0E12A53B3BD42795A2BDD937739E5EB6ECA5CF542206
Stealth Tx P: 02FDF5C5E6B5D3419E42CC0E12A53B3BD42795A2BDD937739E5EB6ECA5CF542206
Shared Secret: 51DD1F217272B5CB4542A62452AFAFDA3745EB573F27541402E45F9A3883F5EC
Key 1: B31BD7791E48B225D978B40A38EB1695CCA6A52EAF425339B6B30C9E02F1310A
Q1': 023CDFDF2DC653B5C33646CDFF36DBD899DEA37A6B431CD412631D5112507D1D5C
Hash160(Q1'): F60730C4E1E0ACF6B7B5C2B6C4E801D18F1B2914
Spend Stealth TX: 01000000011944F941AECC1885A16908D567BF0A495A9C35B60294A149B24759F6D676856E000000006B483045022063F5F9C78F83F0204DA66FB9E957585BF983ED21A617BCF8BC61895D8444F3D9022100F75C2A17EFEE9D13440683978285FC0FD53EE9652F225F0DE0CBFBA64D5F2DD50121023CDFDF2DC653B5C33646CDFF36DBD899DEA37A6B431CD412631D5112507D1D5CFFFFFFFF01C09EE605000000001976A9147DE4593F6CCF83A3B21F1EB7AA4321FC1CE598C088AC00000000
Spend Stealth TxID: 95dd90fe34586f1c499ad3e8e6f6d08b2fcbea933624bf746a25f4f18a046d89
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment