From c14fe6a0acc92f6818079c7609d19b356d6fb5b3 Mon Sep 17 00:00:00 2001 From: Daniel Suo Date: Thu, 18 Dec 2014 16:46:24 -0500 Subject: [PATCH] clean up transaction files --- README.md | 3 + REQUIRE | 2 + src/Coin.jl | 8 +- src/{wif.jl => addresses.jl} | 42 +++++++- src/base58.jl | 37 +++++-- src/keys.jl | 13 ++- src/messages.jl | 30 +++--- src/op.jl | 4 + src/tx.jl | 186 +++++++++++++++++++++++++++++++++++ src/utils.jl | 14 +-- test/runtests.jl | 2 + 11 files changed, 304 insertions(+), 37 deletions(-) rename src/{wif.jl => addresses.jl} (53%) create mode 100644 src/op.jl create mode 100644 src/tx.jl diff --git a/README.md b/README.md index eb2d94d..9b9d53f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ A (self-educational, incomplete, and likely incorrect) library for working with # To Do - https://en.bitcoin.it/wiki/Technical_background_of_Bitcoin_addresses +# TODO: multiple inputs +# TODO: convenience functions +# TODO: clean up and documentation ## Wallet First, we're going to implement a thin-client wallet. diff --git a/REQUIRE b/REQUIRE index 87f0245..26d1c36 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1 +1,3 @@ Coverage +HTTPClient +JSON diff --git a/src/Coin.jl b/src/Coin.jl index 591aa22..980d3ca 100644 --- a/src/Coin.jl +++ b/src/Coin.jl @@ -5,14 +5,16 @@ export generate_keys, get_pub_key, - # wif.jl + # addresses.jl private2wif, wif2private, wif_check_sum, + pub2base58, # base58.jl encode58, decode58, + decode58_to_array, # messages.jl, create_header, @@ -50,9 +52,11 @@ using Crypto include("utils.jl") include("base58.jl") include("keys.jl") -include("wif.jl") +include("addresses.jl") include("messages.jl") include("signatures.jl") +include("op.jl") +include("tx.jl") Crypto.init() diff --git a/src/wif.jl b/src/addresses.jl similarity index 53% rename from src/wif.jl rename to src/addresses.jl index 55f4c6b..d2a89ae 100644 --- a/src/wif.jl +++ b/src/addresses.jl @@ -21,9 +21,9 @@ function private2wif(private_key; network_id = "80", compression = "") private_key = string(network_id, private_key, compression) hashed = Crypto.digest("SHA256", private_key, is_hex=true) - hashed = Crypto.digest("SHA256", hashed, is_hex=true) + hashed = Crypto.digest("SHA256", hashed) - checksum = Crypto.hex_array_to_string(hashed[1:4]) + checksum = Crypto.oct2hex(hashed[1:4]) private_key = string(private_key, checksum) @@ -48,3 +48,41 @@ function wif_check_sum(wif) result = hex(decode58(wif)) return get_checksum(result[1:end-8], is_hex=true) == result[end-8+1:end] end + +function pub2base58(pub_key::String; network_id = "00") + pub_key_length = div(length(pub_key), 2) + + # If public key is elliptic curve coordinate, hash with SHA-256 + if pub_key_length == 65 + pub_key = Crypto.digest("SHA256", pub_key, is_hex = true) + pub_key = Crypto.oct2hex(pub_key) + pub_key_length = div(length(pub_key), 2) + end + + # If public key has been SHA-256 hashed, hash with RIPEMD-160 + if pub_key_length == 32 + pub_key = Crypto.digest("RIPEMD160", pub_key, is_hex = true) + pub_key = Crypto.oct2hex(pub_key) + pub_key_length = div(length(pub_key), 2) + end + + # If public key has been RIPEMD-160 hashed, add network id + if pub_key_length == 20 + pub_key = string(network_id, pub_key) + pub_key_length = div(length(pub_key), 2) + end + + # If public key has network id added, add checksum + if pub_key_length == 21 + checksum = get_checksum(pub_key, is_hex = true) + pub_key = string(pub_key, checksum) + pub_key_length = div(length(pub_key), 2) + end + + # If public key has checksum added + if pub_key_length == 25 + pub_key = encode58(Crypto.hex2oct(pub_key)) + end + + return pub_key +end \ No newline at end of file diff --git a/src/base58.jl b/src/base58.jl index 2e79caf..f63ca6b 100644 --- a/src/base58.jl +++ b/src/base58.jl @@ -23,13 +23,24 @@ const base58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" ############################################################################## # We really only need these for base 58 because Julia's default base 58 math -# uses a different alphabet. Still asking users to supply a base to have nice, -# but descriptive function names. +# uses a different alphabet. +function encode58(n::Array{Uint8}) + result = encode58(Crypto.oct2int(n)) + + # TODO: this isn't correct in the case of general # of 0 bytes + for byte in n + if byte == 0x00 + result = string(base58[1], result) # Add the zero element + end + end + + return result +end + # - n: integer we want to convert -# - b: base we want to convert to -function encode58(n; b = 58) +function encode58(n::Integer) # Require base to be 58 for now - @assert b == 58 + b = 58 output = "" @@ -43,9 +54,9 @@ end # Decode from base 58 to integer # - n: base 58 number we want to convert -function decode58(n; b = 58) +function decode58(n::String) # Require base to be 58 for now - @assert b == 58 + b = 58 result = BigInt(0) @@ -55,3 +66,15 @@ function decode58(n; b = 58) return result end + +function decode58_to_array(n::String) + result = Crypto.hex2oct(hex(decode58(n))) + + # TODO: this isn't correct in the case of general # of 0s + # Capture 2 leading 0s + if n[1:2] == repeat(base58[1], 2) # Get the zero element + result = [0x00, result] + end + + return result +end diff --git a/src/keys.jl b/src/keys.jl index 64dd7d3..d038bcf 100644 --- a/src/keys.jl +++ b/src/keys.jl @@ -12,15 +12,20 @@ ## ############################################################################## +type Keys + priv_key::Array{Uint8} + pub_key::Array{Uint8} +end + function generate_keys(network_id = 0x00, version = "1") priv_key = Crypto.random(256) pub_key = get_pub_key(priv_key, network_id = network_id, version = version) - return (Crypto.hex_array_to_string(priv_key), pub_key) + return (Crypto.oct2hex(priv_key), pub_key) end function get_pub_key(priv_key::String; network_id = 0x00, version = "1") - get_pub_key(Crypto.hex_string_to_array(priv_key), + get_pub_key(Crypto.hex2oct(priv_key), network_id = network_id, version = version) end @@ -40,7 +45,7 @@ function get_pub_key(priv_key::Array{Uint8}; network_id = 0x00, version = "1") # Add version byte in front of RIPEMD-160 hash (0x00 for Main Network) # Reference: https://bitcoin.org/en/developer-reference#address-conversion - pub_key = [bytearray(network_id), pub_key] + pub_key = [Crypto.int2oct(network_id), pub_key] # Get checksum by performing SHA256 hash twice and taking first 4 bytes checksum = get_checksum(pub_key) @@ -53,7 +58,7 @@ function get_pub_key(priv_key::Array{Uint8}; network_id = 0x00, version = "1") # Base58Check encoding. This is the most commonly used Bitcoin Address format # Reference: https://en.bitcoin.it/wiki/Base58Check_encoding # TODO: array to string to BigInt is really round-about - pub_key = parseint(BigInt, Crypto.hex_array_to_string(pub_key), 16) + pub_key = parseint(BigInt, Crypto.oct2hex(pub_key), 16) pub_key = encode58(pub_key) # Append address version byte in hex diff --git a/src/messages.jl b/src/messages.jl index c057de8..79acd5e 100644 --- a/src/messages.jl +++ b/src/messages.jl @@ -68,6 +68,12 @@ import Base.convert # merkleblock # alert +# Define the known magic values +const magic_mainnet = 0xd9b4bef9 +const magic_testnet = 0xdab5bffa +const magic_testnet3 = 0x0709110b +const magic_namecoin = 0xfeb4bef9 + type Message magic::Uint32 # Network identifier command::Array{Uint8} # Message command, right padded with \0 to 12 bytes @@ -81,7 +87,7 @@ type Message checksum = uint32(parseint(get_checksum(payload, is_hex=true)[1:8], 16)) # Turn payload hex string into array of bytes - payload = Crypto.hex_string_to_array(payload) + payload = Crypto.hex2oct(payload) new(magic, command.data, payload) end @@ -92,7 +98,7 @@ type OutPoint index::Uint32 # Index of specific output in tx. 1st output is 0 function OutPoint(hash::String, index::Integer) - OutPoint(Crypto.hex_string_to_array(hash), uint32(index)) + OutPoint(Crypto.hex2oct(hash), uint32(index)) end function OutPoint(hash::Array{Uint8}, index::Integer) @@ -112,7 +118,7 @@ function convert(::Type{Array{Uint8}}, outpoint::OutPoint) result = Array(Uint8, 0) append!(result, reverse(outpoint.hash)) - append!(result, reverse(bytearray(outpoint.index))) + append!(result, reverse(Crypto.int2oct(outpoint.index))) return result end @@ -123,7 +129,7 @@ type Tx_Input sequence::Uint32 # Tx version as defined by the sender function Tx_Input(previous_output::OutPoint, scriptSig::String; sequence = 0xffffffff) - scriptSig = Crypto.hex_string_to_array(scriptSig) + scriptSig = Crypto.hex2oct(scriptSig) Tx_Input(previous_output, scriptSig, sequence = sequence) end @@ -138,7 +144,7 @@ function convert(::Type{Array{Uint8}}, tx_in::Tx_Input) append!(result, bytearray(tx_in.previous_output)) append!(result, reverse(to_varint(length(tx_in.scriptSig)))) append!(result, tx_in.scriptSig) - append!(result, reverse(bytearray(tx_in.sequence))) + append!(result, reverse(Crypto.int2oct(tx_in.sequence))) end type Tx_Output @@ -149,7 +155,7 @@ type Tx_Output # value: transaction value in Satoshi # scriptPubKey: script as hex string function Tx_Output(value, scriptPubKey::String) - scriptPubKey = Crypto.hex_string_to_array(scriptPubKey) + scriptPubKey = Crypto.hex2oct(scriptPubKey) Tx_Output(value, scriptPubKey) end @@ -166,7 +172,7 @@ function convert(::Type{Array{Uint8}}, tx_out::Tx_Output) # TODO: This is a really terrible way to get little # endian byte array - append!(result, reverse(bytearray(tx_out.value))) + append!(result, reverse(Crypto.int2oct(tx_out.value))) append!(result, reverse(to_varint(length(tx_out.scriptPubKey)))) @@ -190,7 +196,7 @@ function convert(::Type{Array{Uint8}}, tx::Tx) result = Array(Uint8, 0) # Add version - append!(result, reverse(bytearray(tx.version))) + append!(result, reverse(Crypto.int2oct(tx.version))) # Add number of inputs append!(result, reverse(to_varint(length(tx.inputs)))) @@ -209,17 +215,11 @@ function convert(::Type{Array{Uint8}}, tx::Tx) end # Add lock_time - append!(result, reverse(bytearray(tx.lock_time))) + append!(result, reverse(Crypto.int2oct(tx.lock_time))) return result end -# Define the known magic values -const magic_mainnet = 0xd9b4bef9 -const magic_testnet = 0xdab5bffa -const magic_testnet3 = 0x0709110b -const magic_namecoin = 0xfeb4bef9 - # const commands = ["version", "verack", "addr", "inv", "getdata", "notfound", # "getblocks", "getheaders", "tx", "block", "headers", # "getaddr", "mempool", "checkorder", "submitorder", "reply", diff --git a/src/op.jl b/src/op.jl new file mode 100644 index 0000000..6bea2ac --- /dev/null +++ b/src/op.jl @@ -0,0 +1,4 @@ +const OP_DUP = 0x76 +const OP_HASH160 = 0xa9 +const OP_EQUALVERIFY = 0x88 +const OP_CHECKSIG = 0xac \ No newline at end of file diff --git a/src/tx.jl b/src/tx.jl new file mode 100644 index 0000000..5df8b29 --- /dev/null +++ b/src/tx.jl @@ -0,0 +1,186 @@ +using Crypto +using HTTPClient +using JSON + +const SIGHASH_ALL = 0x00000001 +const SIGHASH_NONE = 0x00000002 +const SIGHASH_SINGLE = 0x00000003 +const SIGHASH_ANYONECANPAY = 0x00000080 + +# Create transaction from previous OutPoints and outputs +function create_tx(keys::Array{Keys}, + outpoints::Array{OutPoint}, + addresses::Array{String}, + amounts::Array{Integer}; + hash_code = SIGHASH_ALL) + + if length(keys) != length(outpoints) + error("Creating a transaction requires a private key for each input") + end + + if length(addresses) != length(amounts) + ereror("Creating a transation requires an amount for each address") + end + + # Build input objects + inputs = Array(Tx_Input, 0) + for outpoint in outpoints + prev_tx = get_tx(outpoint.hash) + input = Tx_Input(outpoint, prev_tx.outputs[outpoint.index].scriptPubKey) + append!(inputs, input) + end + + # Build output objects + outputs = Array(Tx_Output, 0) + for i in length(addresses) + # First byte is network id, last four bytes are checksum + address = decode58_to_array(addresses[i])[2:end-4] + + # Create scriptPubKey for pay2hash + scriptPubKey = [OP_DUP, OP_HASH160, uint8(length(address)), address, OP_EQUALVERIFY, OP_CHECKSIG] + + # Create output object + output = Tx_Output(amounts[i], scriptPubKey) + append!(outputs, output) + end + + # Build transaction object + tx = Tx(inputs, outputs) + + # Get transaction as byte array + raw_tx = bytearray(tx) + + # Append hash code; see here: https://en.bitcoin.it/wiki/OP_CHECKSIG + append!(raw_tx, reverse(bytearray(hash_code))) + + # Double hash the transaction + hash = Crypto.digest("SHA256", Crypto.digest("SHA256", raw_tx)) + + # Sign the transaction using the private key + for i in 1:length(priv) + + # Sign transation using private key + signature = Crypto.ec_sign(hash, keys[i].priv_key) + + # Build scriptSig + scriptSig = Array(Uint8, 0) + + # Append length of signature as little-endian varint hex + append!(scriptSig, reverse(to_varint(length(signature)))) + + # Append the signature as big-endian + append!(scriptSig, signature) + + # Append hash_code byte + append!(scriptSig, uint8(hash_code)) + + # Append length of public key as little-endian varint hex + append!(scriptSig, reverse(to_varint(length(keys[i].pub_key)))) + + # Append public key + append!(scriptSig, keys[i].pub_key) + + tx.inputs[i].scriptSig = scriptSig + end + +end + +function get_tx(hash::Array{Uint8}) + return get_tx(Crypto.oct2hex(hash)) +end + +function get_tx(hash::String) + const TOSHI_API_TX_URL = "https://bitcoin.toshi.io/api/v0/transactions/" + url = string(TOSHI_API_TX_URL, hash) + + # Get data + # TODO: Add error handling to HTTP GET + result = get(url) + + # Get body data + result = result.body.data + + # Convert from hex array to ASCII string for JSON parsing + result = join([char(x) for x in result]) + + # Parse the JSON + result = JSON.parse(result) + + # Parse json inputs into array of Tx_Input objects + json_inputs = result["inputs"] + inputs = Array(Tx_Input, 0) + for input in json_inputs + outpoint = OutPoint(input["previous_transaction_hash"], input["output_index"]) + tx_input = Tx_Input(outpoint, input["script"]) + append!(inputs, [tx_input]) + end + + # Parse json outputs into array of Tx_Output objects + json_outputs = result["outputs"] + outputs = Array(Tx_Output, 0) + for output in json_outputs + tx_output = Tx_Output(output["amount"], output["script_hex"]) + append!(outputs, [tx_output]) + end + + return Tx(inputs, outputs) +end + +function parse_tx(raw_tx::Array{Uint8}) + return 0 +end + +# Crypto.init() + +# priv_key = Crypto.random(256) +# pub_key = Crypto.ec_pub_key(priv_key) +# keys = [Keys(priv_key, pub_key)] + +# outpoint = OutPoint("f2b3eb2deb76566e7324307cd47c35eeb88413f971d88519859b1834307ecfec", 1) +# address = "1runeksijzfVxyrpiyCY2LCBvYsSiFsCm" +# println(address) +# http://bitcoin.stackexchange.com/questions/2859/how-are-transaction-hashes-calculated +# outpoint = OutPoint("f2b3eb2deb76566e7324307cd47c35eeb88413f971d88519859b1834307ecfec", 1) +# input = Tx_Input(outpoint, "76a914010966776006953d5567439e5e39f86a0d273bee88ac") +# output = Tx_Output(99900000, "76a914097072524438d003d23a2f23edb65aae1bb3e46988ac") + +# tx = Tx([input], [output]) + + + +# hash_code = SIGHASH_ALL + +# raw_tx = bytearray(tx) +# append!(raw_tx, reverse(bytearray(hash_code))) +# hash = Crypto.digest("SHA256", Crypto.digest("SHA256", raw_tx)) + +# data = "0100000001eccf7e3034189b851985d871f91384b8ee357cd47c3024736e5676eb2debb3f2010000001976a914010966776006953d5567439e5e39f86a0d273bee88acffffffff01605af405000000001976a914097072524438d003d23a2f23edb65aae1bb3e46988ac0000000001000000" + + + +# sig = Crypto.ec_sign(hash, priv_key) +# hash_code = uint8(SIGHASH_ALL) + +# scriptSig = Array(Uint8, 0) + +# append!(scriptSig, reverse(to_varint(length(sig)))) +# append!(scriptSig, sig) +# append!(scriptSig, hash_code) + +# append!(scriptSig, reverse(to_varint(length(pub_key)))) +# append!(scriptSig, pub_key) + +# tx.inputs[1].scriptSig = scriptSig + +# send = "1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa" +# # First byte is network id, last four bytes are checksum +# addr = decode58_to_array(send)[2:end-4] + +# scriptPubKey = [OP_DUP, OP_HASH160, uint8(length(addr)), addr, OP_EQUALVERIFY, OP_CHECKSIG] + +# tx.outputs[1].scriptPubKey = scriptPubKey + +# raw_tx = bytearray(tx) + +# # TODO: Read raw_tx to Tx +# # TODO: \ No newline at end of file diff --git a/src/utils.jl b/src/utils.jl index c2861f8..9238ebb 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -17,23 +17,23 @@ function to_varint(x::Integer) if x < 0 error("Negative values for VarInt undefined.") elseif x < 0xfd - append!(result, convert(Array{Uint8}, uint8(x))) + append!(result, Crypto.int2oct(uint8(x))) elseif x <= 0xffff append!(result, [0xfd]) - append!(result, convert(Array{Uint8}, uint16(x))) + append!(result, Crypto.int2oct(uint16(x))) elseif x <= 0xffffffff append!(result, [0xfe]) - append!(result, convert(Array{Uint8}, uint32(x))) + append!(result, Crypto.int2oct(uint32(x))) else append!(result, [0xff]) - append!(result, convert(Array{Uint8}, uint64(x))) + append!(result, Crypto.int2oct(uint64(x))) end return result end function reverse_endian(hex_string::String) - return join([hex(x, 2) for x in reverse(hex_string_to_array(hex_string))]) + return join([hex(x, 2) for x in reverse(hex2oct(hex_string))]) end function reverse_endian(hex_data::Integer) @@ -53,8 +53,8 @@ function reverse_endian(hex_data::Integer) end function get_checksum(message::String; is_hex = false) - create_digest(x) = Crypto.digest("SHA256", x, is_hex = is_hex) - return Crypto.hex_array_to_string(create_digest(create_digest(message))[1:4]) + message = Crypto.digest("SHA256", Crypto.digest("SHA256", message, is_hex = true)) + return Crypto.oct2hex(message[1:4]) end function get_checksum(message::Array{Uint8}) diff --git a/test/runtests.jl b/test/runtests.jl index dc01daa..dc6ae52 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -53,6 +53,8 @@ public_key = get_pub_key(secret_key) # WIF checksum @test wif_check_sum("5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ") +@test pub2base58("0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6") == "16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM" + ############################################################################## ## ## Transaction tests