scripts/coins/wdump.rb
require 'sbdb'
require 'bdb'
require 'set'
require 'digest'
require 'pp'
require 'fileutils'
module B58Encode
extend self
@@__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
@@__b58base = @@__b58chars.bytesize
def self.encode(v)
# encode v, which is a string of bytes, to base58.
long_value = 0
v.chars.to_a.reverse.each_with_index do |c, i|
long_value += (256**i) * c.ord
end
result = ''
while long_value >= @@__b58base do
div, mod = long_value.divmod(@@__b58base)
result = @@__b58chars[mod] + result
long_value = div
end
result = @@__b58chars[long_value] + result
nPad = 0
v.chars.to_a.each do |c|
c == "\0" ? nPad += 1 : break
end
return (@@__b58chars[0] * nPad) + result
end
def self.decode(v, length)
#decode v into a string of len bytes
long_value = 0
v.chars.to_a.reverse.each_with_index do |c, i|
long_value += @@__b58chars.index(c) * (@@__b58base**i)
end
result = ''
while long_value >= 256 do
div, mod = long_value.divmod(256)
result = mod.chr + result
long_value = div
end
result = long_value.chr + result
nPad = 0
v.chars.to_a.each do |c|
c == @@__b58chars[0] ? nPad += 1 : break
end
result = 0.chr * nPad + result
if !length.nil? and result.size != length
return nil
end
return result
end
def hash_160(public_key)
h1 = Digest::SHA256.new.digest(public_key)
h2 = Digest::RMD160.new.digest(h1)
return h2
end
def public_key_to_bc_address(public_key, version = 0)
h160 = hash_160(public_key)
return hash_160_to_bc_address(h160, version)
end
def hash_160_to_bc_address(h160, version = 0)
vh160 = version.chr + h160
h3 = Digest::SHA256.new.digest(Digest::SHA256.new.digest(vh160))
addr = vh160 + h3[0..3]
return self.encode(addr)
end
def bc_address_to_hash_160(addr)
bytes = self.decode(addr, 25)
return bytes[1..20]
end
end
class BCDataStream
attr_reader :read_cursor
attr_reader :buffer
def initialize(string)
@buffer = string
@read_cursor = 0
end
def read_string
# Strings are encoded depending on length:
# 0 to 252 : 1-byte-length followed by bytes (if any)
# 253 to 65,535 : byte'253' 2-byte-length followed by bytes
# 65,536 to 4,294,967,295 : byte '254' 4-byte-length followed by bytes
# greater than 4,294,967,295 : byte '255' 8-byte-length followed by bytes of string
if @buffer.eql? nil
raise "not initialized"
end
begin
length = self.read_compact_size
rescue Exception => e
raise "attempt to read past end of buffer: #{e.message}"
end
return self.read_bytes(length)
end
def read_uint32; return _read_num('L', 4); end
def read_int32; return _read_num('l', 4); end
def read_uint64; return _read_num('Q', 8); end
def read_int64; return _read_num('q', 8); end
def read_boolean; return _read_num('c', 1) == 1; end
def read_bytes(length)
result = @buffer[@read_cursor..@read_cursor+length-1]
@read_cursor += length
return result
rescue Exception => e
raise "attempt to read past end of buffer: #{e.message}"
end
def read_compact_size
size = @buffer[@read_cursor].ord
@read_cursor += 1
if size == 253
size = _read_num('S', 2)
elsif size == 254
size = _read_num('I', 4)
elsif size == 255
size = _read_num('Q', 8)
end
return size
end
def _read_num(format, size)
val = @buffer[@read_cursor..@read_cursor+size].unpack(format).first
@read_cursor += size
return val
end
end
class CoinWallet
attr_reader :count, :version, :default_key, :kinds, :seed, :balance
def initialize(file, kind)
@seed = kind_to_value(kind)
@kinds = Set.new
@count = 0
@version = :unknown
@keys = []
@default_key = nil
@addressbook = []
@transactions = []
@encrypted = false
@balance = 0
load_db(file)
rescue Exception => e
puts e.backtrace.join("\n")
raise "Cannot load Wallet: #{e.message}"
end
def encrypted?
@encrypted
end
def keys(type = :public)
return @keys if type.eql? :all
@addressbook.select {|k| k[:local].eql? true}.collect {|x| x.reject {|v| v == :local}}
end
def addressbook(local = nil)
@addressbook.select {|k| k[:local].eql? local}.collect {|x| x.reject {|v| v == :local}}
end
def transactions
@transactions
end
def own?(key)
@keys.any? {|k| k[:address].eql? key}
end
private
def kind_to_value(kind)
case kind
when :bitcoin
0
when :litecoin
48
when :feathercoin
14
when :namecoin
52
end
end
def load_db(file)
env = SBDB::Env.new '.', SBDB::CREATE | SBDB::Env::INIT_TRANSACTION
db = env.btree file, 'main', :flags => SBDB::RDONLY
@count = db.count
load_entries(db)
db.close
env.close
# remove temporary env files
9.times {|i| FileUtils.rm_rf "__db.00#{i}" }
end
def load_entries(db)
db.each do |k,v|
tuple = parse_key_value(k, v)
next unless tuple
@kinds << tuple[:type]
case tuple[:type]
when :version
@version = tuple[:dump][:version]
when :defaultkey
@default_key = tuple[:dump]
when :key, :wkey, :ckey
@keys << tuple[:dump]
@encrypted = true if tuple[:type].eql? :ckey
when :name
tuple[:dump][:local] = true if @keys.any? {|k| k[:address].eql? tuple[:dump][:address] }
@addressbook << tuple[:dump]
when :tx
@transactions << tuple[:dump]
end
end
# we have finished parsing the whole wallet
# we have all the addresses, we can now fill the :own properties in the out transactions
# thus we can calculate the real amount of the transaction (out - change + fee)
recalculate_tx
end
def parse_key_value(key, value)
kds = BCDataStream.new(key)
vds = BCDataStream.new(value)
type = kds.read_string
hash = {}
case type
when 'version'
hash[:version] = vds.read_uint32
when 'name'
hash[:address] = kds.read_string
hash[:name] = vds.read_string
when 'defaultkey'
key = vds.read_bytes(vds.read_compact_size)
#hash[:key] = key
hash[:address] = B58Encode.public_key_to_bc_address(key, @seed)
when 'key'
key = kds.read_bytes(kds.read_compact_size)
#hash[:key] = key
hash[:address] = B58Encode.public_key_to_bc_address(key, @seed)
#hash['privkey'] = vds.read_bytes(vds.read_compact_size)
when "wkey"
key = kds.read_bytes(kds.read_compact_size)
#hash[:key] = key
hash[:address] = B58Encode.public_key_to_bc_address(key, @seed)
#d['private_key'] = vds.read_bytes(vds.read_compact_size)
#d['created'] = vds.read_int64
#d['expires'] = vds.read_int64
#d['comment'] = vds.read_string
when "ckey"
key = kds.read_bytes(kds.read_compact_size)
#hash[:key] = key
hash[:address] = B58Encode.public_key_to_bc_address(key, @seed)
#hash['crypted_key'] = vds.read_bytes(vds.read_compact_size)
when 'tx'
hash.merge! parse_tx(kds, vds)
end
return {type: type.to_sym, dump: hash}
end
def parse_tx(kds, vds)
hash = {}
id = kds.read_bytes(32)
ctx = CoinTransaction.new(id, vds, self.seed)
hash[:id] = ctx.id
hash[:from] = ctx.from
hash[:to] = ctx.to
hash[:amount] = ctx.amount
hash[:time] = ctx.time
hash[:versus] = ctx.versus
hash[:in] = ctx.in
hash[:out] = ctx.out
return hash
end
def recalculate_tx
@transactions.each do |tx|
# fill in the :own properties which indicate the amount is for an address inside the wallet
tx[:out].map {|x| x[:own] = own?(x[:address])}
# fix the "fromMe" that is incorrect if the wallet was rebuilt with -rescan
tx[:versus] = tx[:out].any? {|x| own?(x[:address])} ? :in : :out
end
@transactions.each do |tx|
tx[:from] = Set.new
# calculate the amounts based on the direction (incoming tx)
if tx[:versus].eql? :in
tx[:amount] = tx[:out].select {|x| x[:own]}.first[:value]
tx[:to] = tx[:out].select {|x| x[:own]}.first[:address]
# if the source is an hash of all zeroes, it was mined directly
if tx[:in].size.eql? 1 and tx[:in].first[:prevout_hash].eql? "0"*64
tx[:from] << "MINED BLOCK"
end
# TODO: calculate the source from the past tx
#tx[:from] = ???
@balance += tx[:amount]
end
# calculate the amounts based on the direction (outgoing tx)
if tx[:versus].eql? :out
tx[:amount] = tx[:out].select {|x| not x[:own]}.first[:value]
tx[:to] = tx[:out].select {|x| not x[:own]}.first[:address]
# calculate the fee based on the in and out tx
if tx[:in].size > 0
tx[:in].each do |txin|
@transactions.each do |prev_tx|
if prev_tx[:id] == txin[:prevout_hash]
txin.merge!(prev_tx[:out][txin[:prevout_index]])
tx[:from] << prev_tx[:out][txin[:prevout_index]][:address]
end
end
end
amount_in = tx[:in].inject(0) {|tot, y| tot += y[:value]}
amount_out = tx[:out].inject(0) {|tot, y| tot += y[:value]}
tx[:fee] = (amount_in - amount_out).round(8)
end
@balance -= (tx[:amount] + tx[:fee])
end
# return an array instead of set
tx[:from] = tx[:from].to_a
end
@balance = @balance.round(8)
end
end
class CoinTransaction
attr_reader :id, :from, :to, :amount, :time, :versus, :in, :out
def initialize(id, vds, seed)
@id = id.reverse.unpack("H*").first
@seed = seed
@in = []
@out = []
tx = parse_tx(vds)
calculate_tx(tx)
rescue Exception => e
raise "Cannot parse Transaction: #{e.message}"
end
def calculate_tx(tx)
tx['txIn'].each do |t|
itx = {}
# search in the previous hash repo
itx[:prevout_hash] = t['prevout_hash'].reverse.unpack('H*').first
itx[:prevout_index] = t['prevout_n']
@in << itx
end
tx['txOut'].each do |t|
next unless t['value']
value = t['value']/1.0e8
address = extract_pubkey(t['scriptPubKey'])
@out << {value: value, address: address} if address
end
@time = tx['timeReceived']
@versus = (tx['fromMe'] == true) ? :out : :in
end
def parse_tx(vds)
h = parse_merkle_tx(vds)
n_vtxPrev = vds.read_compact_size
h['vtxPrev'] = []
(1..n_vtxPrev).each { h['vtxPrev'] << parse_merkle_tx(vds) }
h['mapValue'] = {}
n_mapValue = vds.read_compact_size
(1..n_mapValue).each do
key = vds.read_string
value = vds.read_string
h['mapValue'][key] = value
end
n_orderForm = vds.read_compact_size
h['orderForm'] = []
(1..n_orderForm).each do
first = vds.read_string
second = vds.read_string
h['orderForm'] << [first, second]
end
h['fTimeReceivedIsTxTime'] = vds.read_uint32
h['timeReceived'] = vds.read_uint32
h['fromMe'] = vds.read_boolean
h['spent'] = vds.read_boolean
return h
end
def parse_merkle_tx(vds)
h = parse_transaction(vds)
h['hashBlock'] = vds.read_bytes(32)
n_merkleBranch = vds.read_compact_size
h['merkleBranch'] = vds.read_bytes(32*n_merkleBranch)
h['nIndex'] = vds.read_int32
return h
end
def parse_transaction(vds)
h = {}
start_pos = vds.read_cursor
h['version'] = vds.read_int32
n_vin = vds.read_compact_size
h['txIn'] = []
(1..n_vin).each { h['txIn'] << parse_TxIn(vds) }
n_vout = vds.read_compact_size
h['txOut'] = []
(1..n_vout).each { h['txOut'] << parse_TxOut(vds) }
h['lockTime'] = vds.read_uint32
h['__data__'] = vds.buffer[start_pos..vds.read_cursor-1]
return h
end
def parse_TxIn(vds)
h = {}
h['prevout_hash'] = vds.read_bytes(32)
h['prevout_n'] = vds.read_uint32
h['scriptSig'] = vds.read_bytes(vds.read_compact_size)
h['sequence'] = vds.read_uint32
return h
end
def parse_TxOut(vds)
h = {}
h['value'] = vds.read_int64
h['scriptPubKey'] = vds.read_bytes(vds.read_compact_size)
return h
end
def extract_pubkey(bytes)
# here we should parse the OPCODES and check them, but we are lazy
# and we fake the full parsing... :)
address = nil
case bytes.bytesize
# TODO: implement other opcodes
when 132
# non-generated TxIn transactions push a signature
# (seventy-something bytes) and then their public key
# (33 or 65 bytes) onto the stack:
when 67
# The Genesis Block, self-payments, and pay-by-IP-address payments look like:
# 65 BYTES:... CHECKSIG
when 25
# Pay-by-Bitcoin-address TxOuts look like:
# DUP HASH160 20 BYTES:... EQUALVERIFY CHECKSIG
# [ OP_DUP, OP_HASH160, OP_PUSHDATA4, OP_EQUALVERIFY, OP_CHECKSIG ]
op_prefix = bytes[0..2]
op_suffix = bytes[-2..-1]
if op_prefix.eql? "\x76\xa9\x14".force_encoding('ASCII-8BIT') and
op_suffix.eql? "\x88\xac".force_encoding('ASCII-8BIT')
address = B58Encode.hash_160_to_bc_address(bytes[3..-3], @seed)
end
when 23
# BIP16 TxOuts look like:
# HASH160 20 BYTES:... EQUAL
end
return address
rescue
end
end
begin
puts "dumping..."
#cw = CoinWallet.new('ftc_wallet_enc.dat', :feathercoin)
cw = CoinWallet.new('wallet_lite.dat', :litecoin)
#cw = CoinWallet.new('btc_wallet_enc.dat', :bitcoin)
puts "#{cw.count} entries"
puts "Version: #{cw.version}"
puts "Encrypted: #{cw.encrypted?}"
puts "Entries: #{cw.kinds.inspect}"
puts "Default key: #{cw.default_key}"
puts "Addressbook:"
puts cw.addressbook
puts "Local keys:"
puts cw.keys
puts "Transactions: (#{cw.transactions.size})"
puts cw.transactions
puts "Balance:"
puts cw.balance
rescue Exception => e
puts "ERROR: #{e.message}"
puts e.backtrace.join("\n")
end