haskoin-store-1.9.0: src/Haskoin/Store/Fleet/lua/pool.lua
-- The shared mempool of a fleet: poolScript in Haskoin.Store.Fleet.Redis,
-- and offer, withdraw and advance in Haskoin.Store.Fleet.Pool. The three are
-- one script because they share how a transaction is put in and taken out.
--
-- ARGV[1] is what to do, ARGV[2] the prefix of the keys, and ARGV[3] the
-- size of a transaction id in bytes. An output is the id of the transaction
-- that made it and four more bytes. Lists are their items one after the
-- other.
--
-- A transaction is given as its id, the time it entered a mempool, the
-- outputs that it spends, its body, '1' if it says itself that it can be
-- replaced by fee or '0', the number of its entries by address, and then
-- five arguments for each entry: its kind, the address, an output
-- or '', an amount or '', and a script or ''. The kinds are 't', the
-- transaction touches the address; 's', it spends that output of the
-- address, of that amount; and 'o', it pays that output to the address,
-- with that amount and script.
--
-- 'offer': the tip that the store saw or '', a string with '1' for each
-- output that the transaction spends and that the store finds unspent at
-- the tip and '0' for the others, the transactions that it replaces, and
-- the transaction. Returns {'taken'}, {'known'}, {'conflict', ...},
-- {'orphan', ...} or {'raced'}.
--
-- 'withdraw': the transactions. Returns those that left.
--
-- 'advance': the tip that the store saw or '', the block to move to, the
-- time in milliseconds or '' for the clock of Redis, the size of a block in
-- bytes, '1' to empty the pool instead of changing it or '0', the
-- transactions that are gone, those that are confirmed, the outputs that
-- are spent, the number of transactions that come back, those
-- transactions, and the blocks that the tip moves through, oldest first,
-- or ''. Returns {'moved'}, {'stays'} or {'raced'}. The pool is
-- emptied for a store that cannot say what the blocks between the two tips
-- did, and only when no other store is live: {'raced'} otherwise.
--
-- What the pool holds for each address, kept in step with its transactions:
-- its transactions and the outputs paid to it, both ordered newest first,
-- the outputs of it that are spent, and the sums of all that, which are the
-- summary of Haskoin.Store.Database.Types.MemSummary. It also holds every
-- transaction by the time it entered a mempool, which is the list of the
-- mempool, and by its id alone, to find those whose id starts with some
-- bytes.
--
-- A transaction can be replaced by fee if it says so itself or if it spends
-- from a transaction of the pool that can be. The second changes when a
-- parent is confirmed or comes back from an abandoned block, so the pool
-- keeps the answer apart from the body, and corrects it for what spends a
-- transaction whenever that one comes or goes. An output counts in
-- 'both' while the pool has both the entry that pays it and the entry that
-- spends it, so it is added by the second to come and taken off by the
-- first to go.
--
-- Every move of the tip is counted, in 'fleet:moves'. The cache of extended
-- public keys answers at one move of the tip and not at one block, since
-- the tip can leave a block and come back to it, and what the pool did to
-- the entries on the way is not undone by coming back.
--
-- The cache of extended public keys (xpub.lua) has, for each key that it
-- holds, the transactions of its addresses and the outputs paid to them,
-- those of the pool among them. So what the pool puts in or takes out at an
-- address goes in or out of the entry of every key that has the address,
-- here, in the same step.
local what, prefix, tsize = ARGV[1], ARGV[2], tonumber(ARGV[3])
local osize = tsize + 4
local key = {
tip = prefix .. 'fleet:tip',
moves = prefix .. 'fleet:moves',
events = prefix .. 'fleet:events',
lease = prefix .. 'fleet:lease',
recent = prefix .. 'fleet:recent',
spends = prefix .. 'pool:spends',
spender = prefix .. 'pool:spender',
body = prefix .. 'pool:body',
kids = prefix .. 'pool:kids:',
entries = prefix .. 'pool:entries',
time = prefix .. 'pool:time',
ids = prefix .. 'pool:ids',
own = prefix .. 'pool:own',
rbf = prefix .. 'pool:rbf',
atx = prefix .. 'pool:atx:',
aout = prefix .. 'pool:aout:',
aval = prefix .. 'pool:aval:',
aspent = prefix .. 'pool:aspent:',
asum = prefix .. 'pool:asum:',
of = prefix .. 'fx:of:',
entry = prefix .. 'fx:e:'
}
local function split(s, n)
local items = {}
for i = 1, #s, n do
items[#items + 1] = string.sub(s, i, i + n - 1)
end
return items
end
-- Tell every store of the fleet that something happened: 'b' and a block
-- that the tip moved to or through, 't' and a transaction that entered the
-- pool, 'd' and one that left it unconfirmed. The events go in a stream, in
-- the step that makes the change, so that every store serves the same
-- events in the same order, whatever its own node saw and when. The stream
-- keeps about the last ten thousand.
local function emit(kind, what)
redis.call('XADD', key.events, 'MAXLEN', '~', '10000', '*', 'k', kind, 'h', what)
end
local function member(t)
return redis.call('HEXISTS', key.spends, t) == 1
end
-- the transactions of the pool among these, and all that spend them
local function descendants(from)
local seen, order, todo = {}, {}, {}
for _, t in ipairs(from) do
if member(t) then
todo[#todo + 1] = t
end
end
while #todo > 0 do
local t = table.remove(todo)
if not seen[t] then
seen[t] = true
order[#order + 1] = t
for _, kid in ipairs(redis.call('SMEMBERS', key.kids .. t)) do
todo[#todo + 1] = kid
end
end
end
return order, seen
end
-- a transaction of the arguments, from a position, and the position after
-- it
local function read(at)
local tx = {id = ARGV[at], time = tonumber(ARGV[at + 1]), inputs = ARGV[at + 2], body = ARGV[at + 3]}
tx.own = ARGV[at + 4] == '1'
local entries = {}
local n = tonumber(ARGV[at + 5])
at = at + 6
for i = 1, n do
entries[i] = {ARGV[at], ARGV[at + 1], ARGV[at + 2], ARGV[at + 3], ARGV[at + 4]}
at = at + 5
end
tx.entries = entries
return tx, at
end
local function add(a, field, by)
redis.call('HINCRBY', key.asum .. a, field, string.format('%.0f', by))
end
-- put the entries of a transaction in, with sign 1, or take them out, with
-- sign -1
local function account(t, time, entries, sign)
local touched = {}
for _, e in ipairs(entries) do
local kind, a, o, v = e[1], e[2], e[3], tonumber(e[4])
touched[a] = true
-- the cached keys that have the address
local watching = {}
if kind ~= 's' then
watching = redis.call('SMEMBERS', key.of .. a)
end
if kind == 't' then
if sign > 0 then
redis.call('ZADD', key.atx .. a, -time, t)
else
redis.call('ZREM', key.atx .. a, t)
end
for _, id in ipairs(watching) do
if sign > 0 then
redis.call('ZADD', key.entry .. id .. ':txs', -time, t)
else
redis.call('ZREM', key.entry .. id .. ':txs', t)
end
end
add(a, 'txs', sign)
elseif kind == 's' then
if sign > 0 then
redis.call('HSET', key.aspent .. a, o, e[4])
else
redis.call('HDEL', key.aspent .. a, o)
end
add(a, 'spent', sign * v)
add(a, 'spentCount', sign)
if redis.call('HEXISTS', key.aval .. a, o) == 1 then
add(a, 'both', sign * v)
add(a, 'bothCount', sign)
end
else
if sign > 0 then
redis.call('ZADD', key.aout .. a, -time, o)
redis.call('HSET', key.aval .. a, o, e[4] .. ' ' .. e[5])
else
redis.call('ZREM', key.aout .. a, o)
redis.call('HDEL', key.aval .. a, o)
end
for _, id in ipairs(watching) do
if sign > 0 then
redis.call('ZADD', key.entry .. id .. ':outs', -time, o)
redis.call('HSET', key.entry .. id .. ':oval', o, e[4] .. ' ' .. #a .. ' ' .. a .. e[5])
else
redis.call('ZREM', key.entry .. id .. ':outs', o)
redis.call('HDEL', key.entry .. id .. ':oval', o)
end
end
add(a, 'paid', sign * v)
add(a, 'paidCount', sign)
if redis.call('HEXISTS', key.aspent .. a, o) == 1 then
add(a, 'both', sign * v)
add(a, 'bothCount', sign)
end
end
end
-- an address that no transaction touches has no summary
if sign < 0 then
for a, _ in pairs(touched) do
if tonumber(redis.call('HGET', key.asum .. a, 'txs')) == 0 then
redis.call('DEL', key.asum .. a)
end
end
end
end
-- work out again whether a transaction of the pool can be replaced by
-- fee, and if that changed, the same for what spends it
local function replaceable(t)
local inputs = redis.call('HGET', key.spends, t)
if not inputs then
return
end
local can = redis.call('SISMEMBER', key.own, t) == 1
if not can then
for _, o in ipairs(split(inputs, osize)) do
if redis.call('SISMEMBER', key.rbf, string.sub(o, 1, tsize)) == 1 then
can = true
break
end
end
end
if can ~= (redis.call('SISMEMBER', key.rbf, t) == 1) then
if can then
redis.call('SADD', key.rbf, t)
else
redis.call('SREM', key.rbf, t)
end
for _, kid in ipairs(redis.call('SMEMBERS', key.kids .. t)) do
replaceable(kid)
end
end
end
-- take one transaction out, leaving what spends it: quietly if a block
-- confirmed it, which is no event of the mempool
local function remove(t, quiet)
local inputs = redis.call('HGET', key.spends, t)
if not inputs then
return
end
if not quiet then
emit('d', t)
end
local kept = cmsgpack.unpack(redis.call('HGET', key.entries, t))
account(t, kept[1], kept[2], -1)
redis.call('HDEL', key.entries, t)
redis.call('ZREM', key.time, t)
redis.call('ZREM', key.ids, t)
for _, o in ipairs(split(inputs, osize)) do
redis.call('HDEL', key.spender, o)
redis.call('SREM', key.kids .. string.sub(o, 1, tsize), t)
end
redis.call('HDEL', key.spends, t)
redis.call('HDEL', key.body, t)
redis.call('SREM', key.own, t)
if redis.call('SREM', key.rbf, t) == 1 then
for _, kid in ipairs(redis.call('SMEMBERS', key.kids .. t)) do
replaceable(kid)
end
end
end
local function insert(tx)
local t = tx.id
redis.call('HSET', key.spends, t, tx.inputs)
redis.call('HSET', key.body, t, tx.body)
for _, o in ipairs(split(tx.inputs, osize)) do
redis.call('HSET', key.spender, o, t)
redis.call('SADD', key.kids .. string.sub(o, 1, tsize), t)
end
redis.call('HSET', key.entries, t, cmsgpack.pack({tx.time, tx.entries}))
redis.call('ZADD', key.time, tx.time, t)
redis.call('ZADD', key.ids, 0, t)
account(t, tx.time, tx.entries, 1)
if tx.own then
redis.call('SADD', key.own, t)
end
replaceable(t)
emit('t', t)
end
local function withdraw(from)
local order = descendants(from)
for _, t in ipairs(order) do
remove(t)
end
return order
end
if what == 'withdraw' then
return withdraw(split(ARGV[4], tsize))
end
local tip = redis.call('GET', key.tip) or ''
if tip ~= ARGV[4] then
return {'raced'}
end
if what == 'offer' then
local flags = ARGV[5]
local tx = read(7)
local t, inputs = tx.id, split(tx.inputs, osize)
if member(t) then
return {'known'}
end
-- what it replaces leaves first, but only if it is taken
local leaving, left = descendants(split(ARGV[6], tsize))
local rivals, counted = {}, {}
for _, o in ipairs(inputs) do
local s = redis.call('HGET', key.spender, o)
if s and not left[s] and not counted[s] then
counted[s] = true
rivals[#rivals + 1] = s
end
end
if #rivals > 0 then
table.insert(rivals, 1, 'conflict')
return rivals
end
local orphans = {'orphan'}
for i, o in ipairs(inputs) do
local maker = string.sub(o, 1, tsize)
local made = member(maker) and not left[maker]
if not made and string.sub(flags, i, i) ~= '1' then
orphans[#orphans + 1] = o
end
end
if #orphans > 1 then
return orphans
end
for _, gone in ipairs(leaving) do
remove(gone)
end
insert(tx)
return {'taken'}
end
-- advance: the block must be the one that the live stores agree on
local target, clock, bsize = ARGV[5], ARGV[6], tonumber(ARGV[7])
if target == tip then
return {'stays'}
end
local now
if clock == '' then
local time = redis.call('TIME')
now = time[1] * 1000 + math.floor(time[2] / 1000)
else
now = tonumber(clock)
end
local live = {}
local leases = redis.call('HGETALL', key.lease)
for i = 1, #leases, 2 do
if tonumber(leases[i + 1]) > now then
live[#live + 1] = leases[i]
end
end
if #live == 0 then
return {'raced'}
end
table.sort(live)
local reports = {}
for i, store in ipairs(live) do
reports[i] = redis.call('HGET', key.recent, store) or ''
end
local agreed = ''
for _, block in ipairs(split(reports[1], bsize)) do
local everywhere = true
for j = 2, #reports do
local found = false
for _, other in ipairs(split(reports[j], bsize)) do
if other == block then
found = true
break
end
end
if not found then
everywhere = false
break
end
end
if everywhere then
agreed = block
break
end
end
if agreed ~= target then
return {'raced'}
end
-- a store that is alone and does not know the way from the old tip starts
-- the pool again
if ARGV[8] == '1' then
if #live ~= 1 then
return {'raced'}
end
for _, k in ipairs(redis.call('KEYS', prefix .. 'pool:*')) do
redis.call('DEL', k)
end
redis.call('SET', key.tip, target)
redis.call('INCR', key.moves)
emit('b', target)
return {'moved'}
end
-- the abandoned blocks: what can come back does, and what spends the rest
-- leaves
local at = 13
for _ = 1, tonumber(ARGV[12]) do
local tx
tx, at = read(at)
if not member(tx.id) then
-- it was confirmed: what spends the same in the pool gives way
local rivals = {}
for _, o in ipairs(split(tx.inputs, osize)) do
local s = redis.call('HGET', key.spender, o)
if s then
rivals[#rivals + 1] = s
end
end
withdraw(rivals)
insert(tx)
end
end
local orphaned = {}
for _, t in ipairs(split(ARGV[9], tsize)) do
for _, kid in ipairs(redis.call('SMEMBERS', key.kids .. t)) do
orphaned[#orphaned + 1] = kid
end
end
withdraw(orphaned)
-- the new blocks: what they confirm leaves alone, and what spends what they
-- spend leaves with all that spends it
for _, t in ipairs(split(ARGV[10], tsize)) do
remove(t, true)
end
local clashing = {}
for _, o in ipairs(split(ARGV[11], osize)) do
local s = redis.call('HGET', key.spender, o)
if s then
clashing[#clashing + 1] = s
end
end
withdraw(clashing)
redis.call('SET', key.tip, target)
redis.call('INCR', key.moves)
-- the blocks that the tip moved through, oldest first, if the store that
-- moves it gave them, and the block that it is at now otherwise
local path = split(ARGV[at] or '', bsize)
if #path == 0 then
path = {target}
end
for _, block in ipairs(path) do
emit('b', block)
end
return {'moved'}