Files
rippled/test/ledger-state.coffee
Nicholas Dudfield 373ce72984 Update freeze tests
* Conditionally skip freeze enforcement tests
* Honour remote.fee_cushion setting
* Workaround ripple-lib regression
* Don't use mocha features from wrong version
2014-07-18 10:35:06 -07:00

731 lines
22 KiB
CoffeeScript

################################### REQUIRES ###################################
# This gives coffee-script proper file/lines in the exceptions
async = require 'async'
assert = require 'assert'
{
Amount
Remote
Seed
Base
UInt160
Transaction
sjcl
} = require 'ripple-lib'
testutils = require './testutils'
#################################### HELPERS ###################################
pretty_json = (v) -> JSON.stringify(v, undefined, 2)
exports.TestAccount = class TestAccount
SHA256_RIPEMD160: (bits) ->
sjcl.hash.ripemd160.hash sjcl.hash.sha256.hash(bits)
derive_pair: (passphrase) ->
seed = Seed.from_json(passphrase)
master_seed = seed.to_json()
key_pair = seed.get_key()
pubKey = sjcl.codec.hex.toBits key_pair.to_hex_pub()
address = Base.encode_check(0,
sjcl.codec.bytes.fromBits(@SHA256_RIPEMD160 pubKey))
[address, master_seed, key_pair]
constructor: (passphrase) ->
@passphrase = passphrase
[@address, @master_seed, @key_pair] = @derive_pair(passphrase)
parse_balance_and_trust = (val) ->
reg = new RegExp("([0-9.]+)-([0-9.]+)(/[^/]+/[^/]+)")
m = reg.exec val
if m != null
[m[1] + m[3], m[2] + m[3]]
else
undefined
exports.parse_amount = parse_amount = (amt_val) ->
amt = Amount.from_json(amt_val)
if not amt.is_valid()
amt = Amount.from_human(amt_val)
if not amt.is_valid()
amt = null
amt
exports.Balance = class Balance
'''
Represents a parsed balance declaration, which could represent an xrp balance
or an iou balance and optional limit.
@amount
@limit
@balance
'''
constructor: (value) ->
limited = parse_balance_and_trust value
if limited?
[amount, limit] = limited
@amount = parse_amount amount
@limit = parse_amount limit
@is_native = false
else
@amount = parse_amount value
@is_native = @amount.is_native()
@limit = null
################################################################################
class BulkRequests
constructor: (@remote, @assert, @pretty_json) ->
transactor: (fn, args_list, on_each, callback) ->
if args_list.length == 0
return callback()
if not callback?
callback = on_each
on_each = null
@assert callback?, "Must supply a callback"
finalized = {
n: args_list.length
one: ->
if --finalized.n <= 0
callback()
}
#remote = @remote
async.concatSeries(args_list, ((args, callback) =>
tx = @remote.transaction()
fn.apply(tx, args)
on_each?(args..., tx) # after payment() offer_create() etc so set_flags works
tx.on("proposed", (m) =>
@assert m.engine_result is "tesSUCCESS", "Transactor failure: #{@pretty_json m}"
callback()
# testutils.ledger_close remote, ->
).on('final', (m) =>
finalized.one()
# callback()
)
.on("error", (m) =>
@assert false, @pretty_json m
).submit()
),
=> testutils.ledger_close @remote, ->
)
requester: (fn, args_list, on_each, callback, on_results) ->
if not callback?
callback = on_each
on_each = null
@assert callback?, "Must supply a callback"
async.concatSeries(args_list, ((args, callback) =>
req = fn.apply @remote, args
on_each?(args..., req)
req.on("success", (m) =>
if m.status?
@assert m.status is "success", "requester failure: #{@pretty_json m}"
callback(null, m)
).on("error", (m) =>
@assert false, @pretty_json m
).request()
),
(error, results_list) ->
on_results?(results_list)
callback(error, results_list)
)
################################# ALIAS MANAGER ################################
class AliasManager
constructor: (@config, remote, aliases) ->
'''
@config
includes `accounts` property, with structure same as that exported
in testconfig.js
@remote
a Remote object
@aliases
A list of aliases
'''
@add_accounts_to_config(@config, aliases)
@set_test_account_secrets(remote, @config)
@realias_issuer = @create_issuer_realiaser()
@alias_lookup = @create_alias_lookup()
create_alias_lookup: ->
lookup = {}
for nick,acc of @config.accounts
lookup[acc.account] = nick
lookup
lookup_alias: (address) ->
@alias_lookup[UInt160.json_rewrite address]
pretty_json: (v) =>
@realias_issuer pretty_json(v)
add_accounts_to_config: (config, accounts) ->
for account in accounts
if not config.accounts[account]?
acc = config.accounts[account] = {}
user = new TestAccount(account)
acc.account = user.address
acc.secret = user.master_seed
set_test_account_secrets: (remote, config) ->
# TODO: config.accounts
for nick,acc of config.accounts
# # Index by nickname ...
remote.set_secret nick, acc.secret
# # ... and by account ID
remote.set_secret acc.account, acc.secret
amount_key: (amt) ->
currency = amt.currency().to_json()
issuer = @realias_issuer amt.issuer().to_json()
"#{currency}/#{issuer}"
create_issuer_realiaser: ->
users = @config.accounts
lookup = {}
accounts = []
for name, user of users
accounts.push user.account
lookup[user.account] = name
realias = new RegExp(accounts.join("|"), "g")
(str) -> str.replace(realias, (match) ->lookup[match])
############################# LEDGER STATE COMPILER ############################
exports.LedgerState = class LedgerState
parse_amount: (amt_val) ->
parse_amount(amt_val)
amount_key: (amt) ->
@am.amount_key amt
record_iou: (account_id, amt)->
key = @amount_key amt
@assert @declaration.accounts[key.split('/')[1]]?,
"Account for #{key} does not exist"
a_ious = @ensure account_id, @ious
@assert !a_ious[key]?,
"Account #{account_id} has more than one amount for #{key}"
a_ious[key] = amt
ensure: (account_id, obj, val) ->
if not obj[account_id]?
obj[account_id] = val ? {}
obj[account_id]
record_xrp: (account_id, amt)->
@assert !@accounts[account_id]?,
"Already declared XRP for #{account_id}"
@accounts[account_id] = amt
record_trust: (account_id, amt, is_balance) ->
key = @amount_key amt
a_trusts = @ensure account_id, @trusts_by_ci
if a_trusts[key]? and !is_balance
cmp = amt.compareTo a_trusts[key]
@assert cmp != - 1,
"Account #{account_id} trust is less than balance for #{key}"
a_trusts[key] = amt
compile_explicit_trusts: ->
for account_id, account of @declaration.accounts
if not account.trusts?
continue
for amt_val in account.trusts
amt = @parse_amount amt_val
@assert amt != null and !amt.is_native(),
"Trust amount #{amt_val} specified for #{account_id} is not valid"
@record_trust(account_id, amt, false)
undefined
compile_accounts_balances_and_implicit_trusts: ->
for account_id, account of @declaration.accounts
xrp_balance = null
@assert account.balance?,
"No balance declared for #{account_id}"
for amt_val in account.balance
trust = null
balance_trust = parse_balance_and_trust(amt_val)
if balance_trust?
[amt_val, trust_val] = balance_trust
trust = @parse_amount trust_val
@assert trust != null,
"Trust amount #{trust_val} specified for #{account_id} "
"is not valid"
amt = @parse_amount amt_val
@assert amt != null,
"Balance amount #{amt_val} specified for #{account_id} "
"is not valid"
if amt.is_native()
xrp_balance = @record_xrp(account_id, amt)
else
@record_iou(account_id, amt)
@record_trust(account_id, trust ? amt, true)
@assert xrp_balance,
"No XRP balanced declared for #{account_id}"
undefined
compile_offers: ->
for account_id, account of @declaration.accounts
if not account.offers?
continue
for offer in account.offers
[pays, gets, splat...] = offer
gets_amt = @parse_amount gets
@assert gets_amt != null,
"For account #{account_id} taker_gets amount #{gets} is invalid"
pays_amt = @parse_amount pays
@assert pays_amt != null,
"For account #{account_id} taker_pays amount #{pays} is invalid"
offers = @ensure(account_id, @offers_by_ci)
offers = @ensure(account_id, @offers_by_ci)
offers_all = @ensure('offers', offers, [])
if gets_amt.is_native()
total = offers.xrp_total ?= new Amount.from_json('0')
new_total = total.add(gets_amt)
@assert @accounts[account_id].compareTo(new_total) != - 1,
"Account #{account_id}s doesn't have enough xrp to place #{offer}"
else
key = @amount_key gets_amt
if key.split('/')[1] != account_id
key_offers = @ensure(key, offers, {})
total = key_offers.total ?= Amount.from_json("0/#{key}")
new_total = total.add(gets_amt)
a_ious = @ensure(account_id, @ious)
@assert a_ious[key]?,
"Account #{account_id} doesn't hold any #{key}"
@assert a_ious[key].compareTo(new_total) != - 1,
"Account #{account_id} doesn't have enough #{key}"
" to place #{offer}"
key_offers.total = new_total
offers_all.push [pays_amt, gets_amt, splat...]
@offers = []
for account_id, obj of @offers_by_ci
for offer in obj.offers
sliced = offer[0..]
sliced.unshift account_id
@offers.push sliced
# @offers[account_id] = obj.offers
@offers
base_reserve: ->
@declaration.reserve?.base ? "200.0"
incr_reserve: ->
@declaration.reserve?.base ? "50.0"
check_reserves: ->
base_reserve_amt = @base_reserve()
incr_reserve_amt = @incr_reserve()
base_reserve = @parse_amount base_reserve_amt
inc_reserve = @parse_amount incr_reserve_amt
@assert base_reserve != null,
"Base reserve amount #{base_reserve_amt} is invalid"
@assert base_reserve != null,
"incremental amount #{incr_reserve_amt} is invalid"
for account_id, account of @declaration.accounts
total_needed = base_reserve.clone()
owner_count = 0
offers = @offers_by_ci[account_id]
if offers?
if offers.xrp_total?
total_needed = total_needed.add offers.xrp_total
if offers.offers?
owner_count += @offers_by_ci[account_id].offers.length
if @trusts_by_ci[account_id]?
owner_count += Object.keys(@trusts_by_ci[account_id]).length
owner_count_amount = Amount.from_json(String(owner_count))
inc_reserve_n = owner_count_amount.multiply(inc_reserve)
total_needed = total_needed.add(inc_reserve_n)
@assert @accounts[account_id].compareTo total_needed != - 1,
"Account #{account_id} needs more XRP for reserve"
@reserves[account_id] = total_needed
format_payments: ->
# We do these first as the following @ious need xrp to issue ious ;0
for account_id, xrps of @accounts
@xrp_payments.push ['root', account_id, xrps]
for account_id, ious of @ious
for curr_issuer, amt of ious
src = @realias_issuer amt.issuer().to_json()
dst = account_id
@iou_payments.push [src, dst, amt]
undefined
format_trusts: ->
for account_id, trusts of @trusts_by_ci
for curr_issuer, amt of trusts
@trusts.push [account_id, amt]
undefined
setup_alias_manager: ->
@am = new AliasManager(@config, @remote, Object.keys(@declaration.accounts))
@realias_issuer = @am.realias_issuer
pretty_json: (v) =>
@realias_issuer pretty_json(v)
constructor: (declaration, @assert, @remote, @config) ->
@declaration = declaration
@accounts = {} # {$account_id : $xrp_amt}
@trusts_by_ci = {} # {$account_id : {$currency/$issuer : $iou_amt}}
@ious = {} # {$account_id : {$currency/$issuer : $iou_amt}}
@offers_by_ci = {} # {$account_id : {offers: [], $currency/$issuer : {total: $iou_amt}}}
@reserves = {}
@xrp_payments = [] # {$account_id: []}
@trusts = [] # {$account_id: []}
@iou_payments = [] # {$account_id: []}
@offers = [] # {$account_id: []}
@setup_alias_manager()
@compile_accounts_balances_and_implicit_trusts()
@compile_explicit_trusts()
@compile_offers()
@check_reserves()
@format_payments()
@format_trusts()
@add_transaction_fees()
compile_to_rpc_commands: ->
passphrase = (src) ->
if src == 'root'
'masterpassphrase'
else
src
make_tx_json = (src, tt) ->
{"Account": UInt160.json_rewrite(src), "TransactionType": tt}
submit_line = (src, tx_json) ->
"build/rippled submit #{passphrase(src)} '#{JSON.stringify tx_json}'"
lines = []
ledger_accept = -> lines.push('build/rippled ledger_accept')
for [src, dst, amount] in @xrp_payments
tx_json = make_tx_json(src, 'Payment')
tx_json.Destination = UInt160.json_rewrite dst
tx_json.Amount = amount.to_json()
lines.push submit_line(src, tx_json)
ledger_accept()
for [src, limit] in @trusts
tx_json = make_tx_json(src, 'TrustSet')
tx_json.LimitAmount = limit.to_json()
lines.push submit_line(src, tx_json)
ledger_accept()
for [src, dst, amount] in @iou_payments
tx_json = make_tx_json(src, 'Payment')
tx_json.Destination = UInt160.json_rewrite dst
tx_json.Amount = amount.to_json()
lines.push submit_line(src, tx_json)
ledger_accept()
for [src, pays, gets, flags] in @offers
tx = new Transaction({secrets: {}})
tx.offer_create(src, pays, gets)
tx.set_flags(flags)
# console.log tx.tx_json
# process.exit()
# tx_json = make_tx_json(src, 'OfferCreate')
# tx_json.TakerPays = pays.to_json()
# tx_json.TakerGets = gets.to_json()
lines.push submit_line(src, tx.tx_json)
ledger_accept()
lines.join('\n')
verifier: (decl) ->
new LedgerVerifier(decl ? @declaration, @remote, @config, @assert, @am)
add_transaction_fees: ->
extra_fees = {}
fee = Amount.from_json(@remote.fee_cushion * 10)
for list in [@trusts, @iou_payments, @offers]
for [src, args...] in list
extra = extra_fees[src]
extra = if extra? then extra.add(fee) else fee
extra_fees[src] = extra
for [src, dst, amount], ix in @xrp_payments
if extra_fees[dst]?
@xrp_payments[ix][2] = amount.add(extra_fees[dst])
setup: (log, done) ->
LOG = (m) ->
self.what = m
log(m)
accounts = (k for k,ac of @accounts).sort()
@remote.set_account_seq(seq, 1) for seq in accounts.concat 'root' # <--
accounts_apply_arguments = ([ac] for ac, _ of @accounts)
self = this
Dump = (v) => console.log @pretty_json(v)
Dump = ->
reqs = new BulkRequests(@remote, @assert, @pretty_json)
async.waterfall [
(cb) ->
reqs.transactor(
Transaction::payment,
self.xrp_payments,
((src, dest, amt) ->
LOG("Account `#{src}` creating account `#{dest}` by "+
"making payment of #{amt.to_text_full()}") ),
cb)
(cb) ->
reqs.transactor(
Transaction::ripple_line_set,
self.trusts,
((src, amt) ->
issuer = self.realias_issuer amt.issuer().to_json()
currency = amt.currency().to_json()
LOG("Account `#{src}` trusts account `#{issuer}` for "+
"#{amt.to_text()} #{currency}") ),
cb)
(cb) ->
reqs.transactor(
Transaction::payment,
self.iou_payments,
((src, dest, amt, tx) ->
LOG("Account `#{src}` is making a payment of #{amt.to_text_full()} "+
"to `#{dest}`") ),
cb)
(cb) ->
reqs.transactor(
Transaction::offer_create,
self.offers,
((src, pays, gets, flags, tx) ->
if not tx?
tx = flags
flags = ['Passive']
else
# TODO: icky ;)
delete tx.tx_json.Expiration
tx.set_flags(flags)
LOG("Account `#{src}` is selling #{gets.to_text_full()} "+
"for #{pays.to_text_full()}")),
cb)
(cb) ->
testutils.ledger_close self.remote, cb
], (error) ->
assert !error,
"There was an error @ #{self.what}"
done()
################################ LEDGER VERIFIER ###############################
ensure = (account_id, obj, val) ->
if not obj[account_id]?
obj[account_id] = val ? {}
obj[account_id]
exports.LedgerVerifier = class LedgerVerifier
constructor: (@declaration, @remote, @config, @assert, @am) ->
@am ?= new AliasManager(@config, @remote, Object.keys(@declaration.accounts))
@requester = new BulkRequests(@remote, @assert, @am.pretty_json)
@compile_declaration()
verify_lines: (errors, account_lines) ->
for account in account_lines
# For test sweet ;)
account_alias = @am.lookup_alias account.account
for line in account.lines
peer_alias = @am.lookup_alias line.account
key = "#{line.currency}/#{peer_alias}"
asserted = @iou_balances[account_alias]?[key]
if asserted?
actual = Amount.from_json(
"#{line.balance}/#{line.currency}/#{line.account}")
if not asserted.equals(actual)
balance = (((errors[account_alias] ?= {})['balance'] ?= {}))
balance[key] =
expected: asserted.to_text()
actual: actual.to_text()
asserted = @trusts[account_alias]?[key]
if asserted?
actual = Amount.from_json(
"#{line.limit}/#{line.currency}/#{line.account}")
if not asserted.equals(actual)
limit = (((errors[account_alias] ?= {})['limit'] ?= {}))
limit[key] =
expected: asserted.to_text()
actual: actual.to_text()
verify_infos: (errors, account_infos) ->
for account in account_infos
root = account.account_data
account_alias = @am.lookup_alias root.Account
asserted = @xrp_balances[account_alias]
if asserted?
actual = Amount.from_json root.Balance
if not asserted.equals(actual)
balance = (((errors[account_alias] ?= {})['balance'] ?= {}))
balance['XRP'] =
expected: asserted.to_human()
actual: actual.to_human()
verify_offers: (errors, account_offers) ->
for account in account_offers
account_alias = @am.lookup_alias account.account
get_errors = -> (((errors[account_alias] ?= {})['offers'] ?= []))
assertions = @offers[account_alias]
continue if not assertions?
amount_text = (amt) => @am.realias_issuer amt.to_text_full()
for asserted, ix in assertions
offer = account.offers[ix]
if not offer?
get_errors().push {expected: asserted, actual: 'missing'}
continue
else
# expected_*
[epays, egets] = (parse_amount a for a in asserted)
# actual_*
apays = Amount.from_json offer.taker_pays
agets = Amount.from_json offer.taker_gets
err = {}
if not epays.equals apays
pay_err = (err['taker_pays'] = {})
pay_err['expected'] = amount_text epays
pay_err['actual'] = amount_text apays
if not egets.equals agets
get_err = (err['taker_gets'] = {})
get_err['expected'] = amount_text egets
get_err['actual'] = amount_text agets
if Object.keys(err).length > 0
offer_errors = get_errors()
offer_errors.push err
verify: (account_infos, account_lines, account_offers) ->
errors = {}
# console.log @am.pretty_json account_infos
# console.log @am.pretty_json account_lines
# console.log @am.pretty_json account_offers
@verify_infos errors, account_infos
@verify_lines errors, account_lines
@verify_offers errors, account_offers
errors
do_verify: (done) ->
args_from_keys = (obj) -> ([a] for a in Object.keys obj)
reqs = @requester
lines_args = args_from_keys @iou_balances
info_args = args_from_keys @xrp_balances
offers_args = args_from_keys @offers
async.series [
(cb) ->
reqs.requester(Remote::request_account_info, info_args, cb)
(cb) ->
reqs.requester(Remote::request_account_lines, lines_args, cb)
(cb) ->
reqs.requester(Remote::request_account_offers, offers_args, cb)
], (error, results) =>
assert !error,
"There was an error @ #{error}"
done(@verify(results...))
compile_declaration: ->
@offers = {}
@xrp_balances = {}
@iou_balances = {}
@trusts = {}
@realias_issuer = @am.realias_issuer
record_amount = (account_id, to, amt) =>
key = @am.amount_key amt
ensure(account_id, to)[key] = amt
for account_id, account of @declaration.accounts
if account.offers?
@offers[account_id] = account.offers
if Array.isArray(account.balance)
for value in account.balance
balance = new Balance(value)
if balance.is_native
@xrp_balances[account_id] = balance.amount
else
if balance.limit?
record_amount account_id, @trusts, balance.limit
record_amount account_id, @iou_balances, balance.amount