packages feed

haskoin-store-1.9.0: src/Haskoin/Store/Fleet/lua/xpub.lua

-- The cache of extended public keys of a fleet: xpubScript in
-- Haskoin.Store.Fleet.XPub, which says what an entry is and why. pool.lua
-- keeps the part of each entry that comes from the shared mempool.
--
-- 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. A position in a list is a score, as
-- Haskoin.Store.Fleet.XPub.score gives it: lists are ordered by score and,
-- within one score, by the bytes of the member. What an entry keeps for an
-- output is its amount, a space, the length of its address, a space, the
-- address, and the script.
--
-- The reads take the fleet tip that the store answers from, the id of a
-- key, and the number of its addresses in each of its two chains as the
-- caller has them. They answer {'raced'} if the fleet tip is another block,
-- {'miss'} if the cache is not where the tip is, if it does not have the key,
-- or if it has it with other addresses, and {'ok', ...} otherwise.
--
-- 'bals': the tip, the id, and the time in milliseconds, or '' not to note
-- that the key was used. The numbers of addresses are not given, and not
-- checked. For each address of the entry: the address, its path followed
-- by its balance in the chain if it has one, and its seven sums in the
-- pool separated by spaces or ''. A path is five bytes: the chain of the
-- key that the address is in, and its number there.
--
-- 'txs', 'outs': the tip, the id, the two numbers, where to start, an
-- offset and how many, 0 for all. Where to start is 'top' and two '';
-- 'score' with a score and ''; or 'at' with a score and the bytes that the
-- members of that score are to be no smaller than. 'txs' gives a
-- transaction and a score for each; 'outs' an output, a score and what is
-- kept for it, leaving out the outputs that the pool spends.
--
-- 'count': the tip, the id and the two numbers. The number of transactions.
--
-- 'have': the id. The two numbers of the entry, whatever block the cache is
-- at: {n0, n1}.
--
-- 'build': the tip, the id, the two numbers that the entry is to have
-- before, the time in milliseconds, the number of addresses to add, and for
-- each: the address, its path, its balance in the chain or '', the number
-- of its transactions in the chain, a transaction and a score for each, the
-- number of its unspent outputs in the chain, and an output, a score, an
-- amount and a script for each. What the pool has for the address is added
-- from the pool. Returns {'ok'}; {'raced'} if the fleet tip is another
-- block; {'behind'} if the cache is at another block; {'changed'} if the
-- entry does not have those two numbers of addresses.
--
-- 'drop': the id. Deletes the entry.
--
-- 'tracked': addresses. A string with '1' for each address that an entry
-- has and '0' for the others.
--
-- 'where': nothing. The block that the cache is at, if it is at the fleet
-- tip as it stands, and '' otherwise.
--
-- 'advance': the block that the cache is taken to be at or '', the block to
-- bring it to, '1' to empty it or '0', the number of addresses that the
-- blocks in between changed, and for each: the address, its balance in
-- the chain at the new block or '', the number of transactions that the
-- blocks added to it, a transaction and a score for each, the number of
-- outputs that they paid to it, an output, a score, an amount and a script
-- for each, the number of its outputs that they spent, and those outputs.
-- Returns {'moved'}; {'stays'} if the cache is at the new block already;
-- {'raced'} if the fleet tip is not the new block; and {'emptied'} if it
-- had entries and was asked to empty them, or was not where the tip was
-- one move ago, in which case what happened since cannot be told.
--
-- 'size': nothing. The number of entries, and the number of addresses that
-- they have between them: {entries, addresses}.
--
-- 'evict': how many entries to keep. Deletes the least recently used ones
-- past that many, and says how many it deleted.
local what, prefix, tsize = ARGV[1], ARGV[2], tonumber(ARGV[3])
local key = {
  fleet = prefix .. 'fleet:tip',
  tip = prefix .. 'fx:tip',
  lru = prefix .. 'fx:lru',
  addrs = prefix .. 'fx:addrs',
  of = prefix .. 'fx:of:',
  entry = prefix .. 'fx:e:',
  spender = prefix .. 'pool:spender',
  atx = prefix .. 'pool:atx:',
  aout = prefix .. 'pool:aout:',
  aval = prefix .. 'pool:aval:',
  asum = prefix .. 'pool:asum:'
}

-- whether one string of bytes sorts before another, as Redis sorts the
-- members of a sorted set; the comparison of Lua goes by the locale
local function before(a, b)
  local n = math.min(#a, #b)
  for i = 1, n do
    local x, y = string.byte(a, i), string.byte(b, i)
    if x ~= y then
      return x < y
    end
  end
  return #a < #b
end

local function kept(a, amount, script)
  return amount .. ' ' .. #a .. ' ' .. a .. script
end

local function drop(id)
  local e = key.entry .. id .. ':'
  local had = redis.call('HKEYS', e .. 'addrs')
  for _, a in ipairs(had) do
    redis.call('SREM', key.of .. a, id)
  end
  if #had > 0 then
    redis.call('DECRBY', key.addrs, #had)
  end
  redis.call('DEL', e .. 'addrs', e .. 'meta', e .. 'txs', e .. 'outs', e .. 'oval')
  redis.call('ZREM', key.lru, id)
end

local function empty()
  for _, id in ipairs(redis.call('ZRANGE', key.lru, 0, -1)) do
    drop(id)
  end
  -- whatever a script that failed half way left behind
  for _, k in ipairs(redis.call('KEYS', prefix .. 'fx:*')) do
    redis.call('DEL', k)
  end
end

local function numbers(e)
  local n = redis.call('HMGET', e .. 'meta', 'n0', 'n1')
  return tonumber(n[1] or '0'), tonumber(n[2] or '0')
end

if what == 'have' then
  local n0, n1 = numbers(key.entry .. ARGV[4] .. ':')
  return {n0, n1}
elseif what == 'drop' then
  drop(ARGV[4])
  return {'ok'}
elseif what == 'tracked' then
  local flags = {}
  for i = 4, #ARGV do
    flags[#flags + 1] = tostring(redis.call('EXISTS', key.of .. ARGV[i]))
  end
  return table.concat(flags)
elseif what == 'size' then
  return {redis.call('ZCARD', key.lru), tonumber(redis.call('GET', key.addrs) or '0')}
elseif what == 'evict' then
  local over = redis.call('ZCARD', key.lru) - tonumber(ARGV[4])
  if over <= 0 then
    return 0
  end
  for _, id in ipairs(redis.call('ZRANGE', key.lru, 0, over - 1)) do
    drop(id)
  end
  return over
end

local fleet = redis.call('GET', key.fleet) or ''

-- Where the cache is, is a move of the fleet tip and the block that it went
-- to. A block alone would not do: the tip can leave a block and come back
-- to it, as when a block is reverted and mined again, and a cache that was
-- not brought along would find itself at the tip, with entries that the
-- pool has taken the transactions of that block out of on the way.
local moves = tonumber(redis.call('GET', prefix .. 'fleet:moves') or '0')
local function place(n, block)
  return n .. '@' .. block
end
local here = place(moves, fleet)

if what == 'where' then
  if (redis.call('GET', key.tip) or '') == here then
    return fleet
  end
  return ''
end

if what == 'advance' then
  local from, to = ARGV[4], ARGV[5]
  if fleet ~= to then
    return {'raced'}
  end
  local at = redis.call('GET', key.tip) or ''
  if at == here then
    return {'stays'}
  end
  -- it can be brought along only from the move before this one
  local held = redis.call('ZCARD', key.lru) > 0
  if held and (ARGV[6] == '1' or at ~= place(moves - 1, from)) then
    empty()
    redis.call('SET', key.tip, here)
    return {'emptied'}
  end
  local changed = tonumber(ARGV[7])
  local i = 8
  for _ = 1, changed do
    local a, bal = ARGV[i], ARGV[i + 1]
    local ids = redis.call('SMEMBERS', key.of .. a)
    local txs, outs, spent = {}, {}, {}
    local n = tonumber(ARGV[i + 2])
    i = i + 3
    for _ = 1, n do
      txs[#txs + 1] = {ARGV[i], ARGV[i + 1]}
      i = i + 2
    end
    n = tonumber(ARGV[i])
    i = i + 1
    for _ = 1, n do
      outs[#outs + 1] = {ARGV[i], ARGV[i + 1], kept(a, ARGV[i + 2], ARGV[i + 3])}
      i = i + 4
    end
    n = tonumber(ARGV[i])
    i = i + 1
    for _ = 1, n do
      spent[#spent + 1] = ARGV[i]
      i = i + 1
    end
    for _, id in ipairs(ids) do
      local e = key.entry .. id .. ':'
      local row = redis.call('HGET', e .. 'addrs', a)
      if row then
        redis.call('HSET', e .. 'addrs', a, string.sub(row, 1, 5) .. bal)
      end
      for _, t in ipairs(txs) do
        redis.call('ZADD', e .. 'txs', t[2], t[1])
      end
      for _, o in ipairs(outs) do
        redis.call('ZADD', e .. 'outs', o[2], o[1])
        redis.call('HSET', e .. 'oval', o[1], o[3])
      end
      for _, o in ipairs(spent) do
        redis.call('ZREM', e .. 'outs', o)
        redis.call('HDEL', e .. 'oval', o)
      end
    end
  end
  redis.call('SET', key.tip, here)
  return {'moved'}
end

-- everything else is for a store that answers from the fleet tip
local tip, id = ARGV[4], ARGV[5]
local e = key.entry .. id .. ':'
if fleet ~= tip then
  return {'raced'}
end

if what == 'build' then
  local at = redis.call('GET', key.tip)
  if not at then
    -- nothing is cached yet: the cache starts here
    redis.call('SET', key.tip, here)
  elseif at ~= here then
    return {'behind'}
  end
  local n0, n1 = numbers(e)
  if n0 ~= tonumber(ARGV[6]) or n1 ~= tonumber(ARGV[7]) then
    return {'changed'}
  end
  redis.call('ZADD', key.lru, ARGV[8], id)
  local i = 10
  for _ = 1, tonumber(ARGV[9]) do
    local a, path, bal = ARGV[i], ARGV[i + 1], ARGV[i + 2]
    redis.call('HSET', e .. 'addrs', a, path .. bal)
    redis.call('SADD', key.of .. a, id)
    if string.byte(path, 1) == 0 then
      n0 = n0 + 1
    else
      n1 = n1 + 1
    end
    local n = tonumber(ARGV[i + 3])
    i = i + 4
    for _ = 1, n do
      redis.call('ZADD', e .. 'txs', ARGV[i + 1], ARGV[i])
      i = i + 2
    end
    n = tonumber(ARGV[i])
    i = i + 1
    for _ = 1, n do
      redis.call('ZADD', e .. 'outs', ARGV[i + 1], ARGV[i])
      redis.call('HSET', e .. 'oval', ARGV[i], kept(a, ARGV[i + 2], ARGV[i + 3]))
      i = i + 4
    end
    -- what the pool has for the address, which pool.lua keeps up from now
    local rows = redis.call('ZRANGE', key.atx .. a, 0, -1, 'WITHSCORES')
    for j = 1, #rows, 2 do
      redis.call('ZADD', e .. 'txs', rows[j + 1], rows[j])
    end
    rows = redis.call('ZRANGE', key.aout .. a, 0, -1, 'WITHSCORES')
    for j = 1, #rows, 2 do
      local o = rows[j]
      local value = redis.call('HGET', key.aval .. a, o)
      local space = string.find(value, ' ', 1, true)
      redis.call('ZADD', e .. 'outs', rows[j + 1], o)
      redis.call('HSET', e .. 'oval', o, kept(a, string.sub(value, 1, space - 1), string.sub(value, space + 1)))
    end
  end
  redis.call('HSET', e .. 'meta', 'n0', n0, 'n1', n1)
  redis.call('INCRBY', key.addrs, tonumber(ARGV[9]))
  return {'ok'}
end

if (redis.call('GET', key.tip) or '') ~= here then
  return {'miss'}
end
local answer = {'ok'}

if what == 'bals' then
  local addrs = redis.call('HGETALL', e .. 'addrs')
  if #addrs == 0 then
    return {'miss'}
  end
  if ARGV[6] ~= '' then
    redis.call('ZADD', key.lru, ARGV[6], id)
  end
  -- the pool has a summary for an address when one of its transactions
  -- touches it, and those transactions are in the entry, with scores
  -- below zero: most keys have none, and are spared a read for each
  -- address
  local touched = redis.call('ZCOUNT', e .. 'txs', '-inf', '(0') > 0
  for i = 1, #addrs, 2 do
    local a = addrs[i]
    answer[#answer + 1] = a
    answer[#answer + 1] = addrs[i + 1]
    local sums = ''
    if touched then
      local s = redis.call('HMGET', key.asum .. a,
        'txs', 'paid', 'paidCount', 'spent', 'spentCount', 'both', 'bothCount')
      if s[1] then
        for j = 1, 7 do
          s[j] = s[j] or '0'
        end
        sums = table.concat(s, ' ')
      end
    end
    answer[#answer + 1] = sums
  end
  return answer
end

local n0, n1 = numbers(e)
if n0 + n1 == 0 or n0 ~= tonumber(ARGV[6]) or n1 ~= tonumber(ARGV[7]) then
  return {'miss'}
end

if what == 'count' then
  return {'ok', tostring(redis.call('ZCARD', e .. 'txs'))}
end

-- 'txs' and 'outs': the place in the list where the page starts
local zkey = e .. what
local mode, score, bytes = ARGV[8], ARGV[9], ARGV[10]
local offset, many = tonumber(ARGV[11]), tonumber(ARGV[12])
local first = 0
if mode ~= 'top' then
  first = redis.call('ZCOUNT', zkey, '-inf', '(' .. score)
  if mode == 'at' then
    for _, m in ipairs(redis.call('ZRANGEBYSCORE', zkey, score, score)) do
      if before(m, bytes) then
        first = first + 1
      end
    end
  end
end

if what == 'txs' then
  local last = -1
  if many > 0 then
    last = first + offset + many - 1
  end
  local rows = redis.call('ZRANGE', zkey, first + offset, last, 'WITHSCORES')
  for _, row in ipairs(rows) do
    answer[#answer + 1] = row
  end
  return answer
end

-- 'outs': those that the pool spends are left out, before the offset
local found, skipped, size = 0, 0, 64
while many == 0 or found < many do
  local rows = redis.call('ZRANGE', zkey, first, first + size - 1, 'WITHSCORES')
  if #rows == 0 then
    break
  end
  for j = 1, #rows, 2 do
    local o = rows[j]
    if redis.call('HEXISTS', key.spender, o) == 0 then
      if skipped < offset then
        skipped = skipped + 1
      elseif many == 0 or found < many then
        found = found + 1
        answer[#answer + 1] = o
        answer[#answer + 1] = rows[j + 1]
        answer[#answer + 1] = redis.call('HGET', e .. 'oval', o)
      end
    end
  end
  first = first + size
  size = math.min(4096, size * 4)
end
return answer