Coins are keys
Nobody holds a bitcoin the way you hold a coin. What you hold is a private key, 32 secret bytes, and whoever has the key can spend what it controls. CoinXT is the xTalk Suite's layer over trezor-crypto and libsecp256k1, the code that hardware wallets and Bitcoin itself run: it makes keys, addresses, recovery words and signatures for Bitcoin and Ethereum. Its handlers begin with cx.
Hashes
Coins run on hashes (fingerprints), 32 bytes each. cxSha256 is the hash Bitcoin uses, cxHash256 runs it twice, which is how Bitcoin names blocks and transactions, and cxKeccak256 is Ethereum's. cxHexEncode spells bytes as hex:
put textEncode("Hello, Bitcoin", "UTF-8") into tData
put cxHexEncode(cxSha256(tData))
put cxHexEncode(cxHash256(tData))
put cxHexEncode(cxKeccak256(tData))
put the number of bytes in cxSha256(empty)
d8125d715a1f87012093af8dfd389e1d96d9c68ab58bf024a7a8d9e73056724b 644055e4a5a523bccf09f7295e56e8d4aa60dfbb8eb028a1cd6c8b805df3e907 1d6bbf77c5ed6566afd8bc4c95de519455d0623c00525b3bfd7d8d341558e9f8 32
Private and public keys
A private key is 32 bytes, almost any 32 bytes; cxSeckeyIsValid checks. From it, cxPublicKey makes the public key, which is safe to share and from which nobody can work back to the private one: 33 bytes in its short (compressed) form, 65 in its long form, which Ethereum uses:
-- a key made from a phrase: for learning only
put cxSha256(textEncode("my first key", "UTF-8")) into tKey
put cxSeckeyIsValid(tKey)
put cxPublicKey(tKey, true) into tPublic
put the number of bytes in tPublic
put cxHexEncode(tPublic)
put the number of bytes in cxPublicKey(tKey, false)
true 33 03e47416d316a13dbbdde7a3060f9438c9c3028c256af835194bf29acd91866165 65
Addresses
An address is where people send you coins, made from your public key. One Bitcoin key has several kinds, and the first letters say which: cxBtcAddressP2PKH gives the oldest kind, starting with 1; cxBtcAddressP2WPKH gives SegWit, starting with bc1q, cheaper to spend from; and cxBtcAddressP2TRFromInternal gives Taproot, starting with bc1p, from the 32-byte "x-only" key of cxXOnlyPubkey. An Ethereum address is the end of the Keccak hash of the long public key, after 0x:
put cxSha256(textEncode("my first key", "UTF-8")) into tKey
put cxPublicKey(tKey, true) into tPublic
put cxBtcAddressP2PKH(tPublic)
put cxBtcAddressP2WPKH(tPublic)
put cxBtcAddressP2TRFromInternal(cxXOnlyPubkey(tKey), empty)
put cxEthAddress(cxPublicKey(tKey, false))
18GvyZLsvxUY7WgPANwnNzqxkvDy9DNeQa bc1qflxz6y7ej9ccyn87pxtn3yv4ylvcrycxghpq2u bc1pnc3xf8pvf9kgaslxwrer4ady8d9hq5z2gkl2cxrdvmfanl803uzqcg72du 0x740c064c79e895ef9617cca9cf7079f2bb1e7526
Catching typos
A mistyped address sends coins nowhere, for ever, so each kind carries a checksum: letters worked out from the rest, which a typo makes disagree. Ethereum hides its checksum in the capital letters: cxEthAddressChecksum writes them, and cxEthAddressIsChecksummed checks them. cxWifEncode writes a private key as WIF text, which wallets import, and cxBase58CheckDecode stops with an error when a letter of such text is wrong:
put cxSha256(textEncode("my first key", "UTF-8")) into tKey
put cxEthAddressChecksum(cxEthAddress(cxPublicKey(tKey, false))) into tAddress
put tAddress
put cxEthAddressIsChecksummed(tAddress)
put "c" into char 6 of tAddress
put cxEthAddressIsChecksummed(tAddress)
put cxWifEncode(cxHexEncode(tKey), "mainnet", true) into tWif
put cxWifDecode(tWif) into tDecoded
put tDecoded["seckey"] is cxHexEncode(tKey)
put "X" into char 10 of tWif
try
get cxBase58CheckDecode(tWif)
put "a typo got through!"
catch tError
put "a typo: refused"
end try
0x740C064c79E895eF9617CCa9cF7079F2BB1E7526 true false true a typo: refused
A SegWit address checks its network as well: cxSegwitAddressDecode takes one apart for the network you name, "bc" for real Bitcoin or "tb" for the test network, and refuses the other, so a test wallet cannot send real coins by mistake:
put "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" into tAddress
put cxSegwitAddressDecode("bc", tAddress) into tParts
put "version" && tParts["version"] & "," && the number of bytes in tParts["program"] && "bytes"
put cxSegwitAddressEncode("bc", 0, tParts["program"]) is tAddress
try
get cxSegwitAddressDecode("tb", tAddress)
catch tError
put "the wrong network: refused"
end try
version 0, 20 bytes true the wrong network: refused
Twelve words
A wallet is backed up as a recovery phrase: twelve or twenty-four ordinary words, written on paper, that rebuild it on any device. cxMnemonicFromEntropy turns 16 random bytes into 12 words from a list of 2048, and cxMnemonicValidate checks them: the last word holds a checksum, so a wrong word is caught. Sixteen zero bytes are a famous test value:
put cxMnemonicFromEntropy(cxHexDecode("00000000000000000000000000000000")) into tWords
put the number of words of tWords
put tWords
put cxMnemonicValidate(tWords)
put "zoo" into word -1 of tWords
put cxMnemonicValidate(tWords)
12 abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about true false
One phrase grows a whole tree of keys. cxMnemonicToSeed turns the words (and an optional passphrase) into a 64-byte seed, cxHdFromSeed makes the root of the tree, and cxHdDerivePath walks a path such as m/84'/0'/0'/0/0 to one key. The last number counts up, a new address each time, and the same words always grow the same tree, which is how a new wallet finds every old address:
put "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" into tWords
put cxMnemonicToSeed(tWords, empty) into tSeed
put the number of bytes in tSeed
put cxHdFromSeed(tSeed) into tRoot
repeat with tIndex = 0 to 2
put cxHdDerivePath(tRoot, "m/84'/0'/0'/0/" & tIndex) into tNode
put tIndex & ":" && cxBtcAddressP2WPKH(tNode["pubkey"])
end repeat
64 0: bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu 1: bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g 2: bc1qp59yckz4ae5c4efgw2s5wfyvrz0ala7rgvuz8z
Signing
To spend, you sign: you prove you hold the private key without showing it. A signature covers exactly 32 bytes, so you sign the hash of the message. cxSign signs, and cxVerify checks with the public key: anyone can check, nobody else can sign. Ethereum's cxSignRecoverable adds one byte from which cxRecover finds the signer's public key, so a message need not say who sent it; and Taproot's Schnorr signatures (cxSchnorrSign, cxSchnorrVerify) use the 32-byte x-only key:
put cxSha256(textEncode("my first key", "UTF-8")) into tKey
put cxPublicKey(tKey, true) into tPublic
put cxSha256(textEncode("Pay Bob 5", "UTF-8")) into tDigest
put cxSign(tKey, tDigest) into tSignature
put the number of bytes in tSignature
put cxVerify(tPublic, tDigest, tSignature)
put cxVerify(tPublic, cxSha256(textEncode("Pay Bob 500", "UTF-8")), tSignature)
put cxSignRecoverable(tKey, tDigest) into tSignature
put cxEthAddress(cxRecover(tSignature, tDigest)) is cxEthAddress(cxPublicKey(tKey, false))
put cxSchnorrSign(tKey, tDigest, empty) into tSignature
put cxSchnorrVerify(cxXOnlyPubkey(tKey), tDigest, tSignature)
64 true false true true
Always compare a recovered key with the one you expected: any 65 bytes recover some key.
Transactions
A Bitcoin transaction spends earlier outputs and makes new ones. An input points at an earlier output, by its transaction's id and its number (cxBtcOutpoint); an output is an amount in satoshi, a hundred-millionth of a bitcoin, and the script that locks it (cxBtcOutput). cxBtcTxid gives the new transaction's id, the hash that names it on every block explorer:
put "4ce18f49ba153a51bcda9bb80d7f978e3de6e81b5fc326f00465d4a9b0d32a2b" into tPrevious
put cxHexEncode(cxBtcOutpoint(tPrevious, 0)) into tInput
put cxHexEncode(cxBtcOutput(50000, "0014751e76e8199196d454941c45d1b3a323f1433bd6")) into tOutput
put the length of tInput / 2 && "bytes in"
put the length of tOutput / 2 && "bytes out"
-- version 2, the input's sequence, then the output, and lock time 0
put cxBtcTxid(2, tInput, empty, 4294967295, tOutput, 0)
36 bytes in 31 bytes out c73feaa3700e7091e724775123c4955d710a10319bbbc900553f7e576bad9dd3
CoinXT also builds and signs Ethereum payments (cxEth1559Sighash, cxEth1559Encode); the Dictionary has every handler, and Learn CoinXT (Help > Examples) walks through them.
Try it yourself
Write a function looksLikeBitcoinAddress that says whether text is a valid SegWit address on the real Bitcoin network, without stopping with an error for text that is not.
Show a solution
function looksLikeBitcoinAddress pText
try
get cxSegwitAddressDecode("bc", pText)
return true
catch tError
return false
end try
end looksLikeBitcoinAddress
put looksLikeBitcoinAddress("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
put looksLikeBitcoinAddress("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5")
put looksLikeBitcoinAddress("hello")
true false false
Words in this lesson
cxSha256cxHash256cxKeccak256cxHexEncodecxHexDecodecxPublicKeycxSeckeyIsValidcxBtcAddressP2PKHcxBtcAddressP2WPKHcxBtcAddressP2TRFromInternalcxXOnlyPubkeycxEthAddresscxEthAddressChecksumcxWifEncodecxSegwitAddressDecodecxMnemonicFromEntropycxMnemonicValidatecxMnemonicToSeedcxHdFromSeedcxHdDerivePathcxSigncxVerifycxSignRecoverablecxRecovercxSchnorrSigncxSchnorrVerifycxBtcTxidEach one finds every lesson that uses it. The Dictionary has the whole story of each, with its syntax and more examples.