Years ago I designed a .NET module which facilitates the transmission of BTC to my customers. It creates a binary representation of the desired transaction based on the material presented here and here:
The binary representation is then converted to hex and pushed out through various free services like BlockExplorer, BlockCypher, etc.
The system has been running flawlessly for years. Until yesterday. A customer requested that 0.0284377 BTC to be sent to 38MRMGjMBMp4k7vZhKLHhcM9Pm8AMLy18v. He never received it. Sure enough, he was right. It was never sent.
I looked through all of my logs and saw that my software did in fact submit a transaction with the following inputs and outputs:
- input: 13P38hMYJXFdxDJJn8TtPUJZFXmcpf2J99
- output #1: 38MRMGjMBMp4k7vZhKLHhcM9Pm8AMLy18v (my customer)
- output #2: 1Ny3CV3rAsNMWpLfpxhXW3Fh71YmMEXXU7 (my change address)
Everything looked fine on my side but sure enough, when I looked on block explorer, what I saw shocked me. Consider transaction 9a138b14dcc8ae740073c06933ae04e3b08fe6be6ada0dc175e6484250dfe269 and look at the inputs and outputs:
As you can see, my input is correct. My change address XU7 is also correct. But now look at my customer's address. It says 17fQRjEudTVgexE8aDfhGyzDFEqSnnJJCA (which I'll refer to as JCA) instead of the expected 38MRMGjMBMp4k7vZhKLHhcM9Pm8AMLy18v (which I'll refer to as 18v). What the heck? The JCA address is known neither to me nor to my customer. It's a completely unknown address.
Clearly the problem is in my code somewhere so I dug deeper to find out what made this particular transaction unique. I identified that his desired address (18v) begins with a '3' whereas every other outbound transaction I've completed throughout the history of my app has targeted addresses beginning with '1'.
My stopgap measure was to force users to only specify addresses that begin with a 1. But that's a temporary solution. I need to find out what I'm doing wrong.
I turned to Google. Research indicated that addresses beginning with '3' are typically SegWit addresses where as those beginning with '1' are traditional, old-school addresses. I remember all the talk about SegWit a few months back but I figured it didn't affect me and my legacy transactions would still clear properly. Obviously that was a bad judgement call on my part so now I have to figure out what I did incorrectly and where the bogus JCA address came from.
That's where I'm stuck. I think my problem might have to do with uncompressed vs. compressed public keys based upon discussions here and here:
Here's what I do know: When it comes time to create the outputs for my transaction, my code does the following (explanation follows):
Func<String,byte[]> makeOutScript=(btcAddrHex)=>{
byte[] addrBytes=BTCUtils.Base58Decode(btcAddrHex);
byte[] pubKeyBytes=addrBytes.Take(addrBytes.Length-4).Skip(1).ToArray();
using(MemoryStream ms=new MemoryStream()){
using(BinaryWriter bw=new BinaryWriter(ms)){
bw.Write((byte)0x76); //op_dup
bw.Write((byte)0xa9); //op_hash160
bw.Write((byte)20); //size of public key
bw.Write(pubKeyBytes); //public key
bw.Write((byte)0x88); //op_equalverify
bw.Write((byte)0xac); //op_checksig
bw.Flush();
return ms.ToArray();
}
}
};
What I'm doing here is: Converting the output address to bytes by Base58 decoding it. I strip off the last four bytes, which are the checksum, and the first byte, which is always 0x00 (apparently a network indicator of some sort). That leaves me with the raw public key. Actually, looking at my internal notes, it's not actually the public key but rather 0x04 prepended to the public key and then passed through SHA256 and then RIPEMD160. But I'll refer to this blob as the public key. From there the byte sequence is 0x76, 0xa9, 20 (the size of the public key blob), the public key blob itself, 0x88, and 0xac.
I don't pretend to understand what all those single-byte entries mean but the blog posts above said to include them so that's what I did, and until now it's worked just fine.
The question is: How on earth did the 18v address get transformed into the JCA address when it was submitted to the network? Is my "size of public key" value (20) incorrect, perhaps? At a gut level, it does seem odd that a compressed (18v) and uncompressed (JCA) public key would both have a constant size of 20. But maybe I'm on the wrong track entirely and compression has nothing to do with it.
I don't have to tell you how awful it would be if this transaction were 100 BTC instead of 0.0284377. I lucked out that my bug was triggered on a low value transaction. But I really want to solve this. Can you point me down the right path?