fix: ladder force-select reserve_pub extract (1.20.2)

Wallet accept/tx dumps mix log lines with JSON; json.loads from the
first brace always failed, and regex only scanned accept (no reservePub).
Parse with JSONDecoder.raw_decode, scrape both files, score by WID/amount.
Prefer Homebrew python for taler-helper-sqlite3; find_wallet_cli prefers
ts-core 1.6.x. Soft-skip max-1 mint 5110/P0001 as ceiling.

Verified on live GOA: mid rungs force-select HTTP 200 + confirm 204 → OK_BANK.
This commit is contained in:
Hernâni Marques 2026-07-19 13:45:19 +02:00
parent 41021f14f7
commit c477a3aec8
No known key found for this signature in database
5 changed files with 135 additions and 70 deletions

View file

@ -171,10 +171,17 @@ ladder_over() {
}
wcli() {
# taler-helper-sqlite3 is Python ≥3.11; prefer Homebrew/local python on macOS
# so Apple /usr/bin/python3 3.9 does not FATAL the sqlite backend (no reservePub).
local _path="${PATH:-}"
case ":${_path}:" in
*:/opt/homebrew/bin:*) ;;
*) _path="/opt/homebrew/bin:/usr/local/bin:${_path}" ;;
esac
if [ -n "${CLI_JS:-}" ] && [ -f "$CLI_JS" ]; then
node "$CLI_JS" --wallet-db="$WDB" --no-throttle "$@"
PATH="$_path" node "$CLI_JS" --wallet-db="$WDB" --no-throttle "$@"
else
taler-wallet-cli --wallet-db="$WDB" --no-throttle "$@"
PATH="$_path" taler-wallet-cli --wallet-db="$WDB" --no-throttle "$@"
fi
}
@ -238,10 +245,18 @@ wallet_avail() {
wcli balance 2>/dev/null | python3 -c '
import json,sys
t=sys.stdin.read()
dec=json.JSONDecoder()
i=t.find("{")
if i<0:
print("0"); raise SystemExit
d=json.loads(t[i:t.rfind("}")+1])
try:
d,_=dec.raw_decode(t,i)
except Exception:
# last-resort: brace slice (may fail on trailing logs)
try:
d=json.loads(t[i:t.rfind("}")+1])
except Exception:
print("0"); raise SystemExit
cur=sys.argv[1]
for b in d.get("balances") or []:
a=b.get("available") or ""
@ -587,6 +602,16 @@ for AMT in "$@"; do
echo -e "${rung}\t${range_note}\t${AMT}\t${status}\t${ms_mint}\t${ms_accept}\t${ms_confirm}\t${ms_settle}\t${ms_total}\t${WID}\t${note}" >>"$TSV"
continue
fi
# max-1 can also hit the same libeufin SQL ceiling (5110/P0001) on live GOA —
# treat as soft ceiling when mid rungs already passed; do not block the report.
if [ "$IS_MAX_M1_PIN" = "1" ] && echo "$note" | grep -qE '5110|P0001'; then
status="CEILING_REJECT"
note="max-1 rejected (same ceiling as absolute max): $note"
warn bank "mint $AMT rejected (max-1 ceiling)" \
"problem: bank rejects max-1 with SQL P0001/5110 (pool ceiling). Mid-rung OK_BANK/OK still count. Ladder continues. detail: $note"
echo -e "${rung}\t${range_note}\t${AMT}\t${status}\t${ms_mint}\t${ms_accept}\t${ms_confirm}\t${ms_settle}\t${ms_total}\t${WID}\t${note}" >>"$TSV"
continue
fi
err bank "mint $AMT failed" "$note"
status="FAIL_MINT"
STOP_REASON="$note"
@ -648,6 +673,10 @@ for AMT in "$@"; do
# Collect reserve_pub candidates for *this* withdrawal (WID + amount).
# Cumulative wallets re-print old reserves in accept/tx dumps — never trust a single "last" blindly.
# Prints unique pubs one per line, preferred order first.
#
# Critical: wallet-cli dumps mix log lines + JSON. Never json.loads(rest_of_file) —
# trailing "Shutdown requested" lines make that always fail. Use JSONDecoder.raw_decode
# and regex on *both* accept and transactions output (reservePub lives in tx, not accept).
extract_rpubs_for_wid() {
python3 - "$WID" "$AMT" "$SCRATCH/accept-$tag.out" "$SCRATCH/tx-$tag.json" "$SCRATCH/used-rpubs.txt" <<'PY'
import json, re, sys
@ -661,99 +690,118 @@ used = set()
if Path(used_path).is_file():
used = {ln.strip() for ln in open(used_path) if ln.strip()}
amt_num = amt.split(":", 1)[-1] if amt else ""
dec = json.JSONDecoder()
def walk_collect(o, bag, ctx=None):
ctx = dict(ctx or {})
if isinstance(o, dict):
# First pass: sibling scalars (reservePub + bankConfirmationUrl + amounts
# are peers under withdrawalDetails — order in JSON must not matter).
for k, v in o.items():
kl = str(k).lower()
if kl in ("withdrawal_id", "withdraw_id", "wopid", "id") and isinstance(v, str):
if not isinstance(v, str):
continue
kl = str(k).lower().replace("-", "_")
if kl in (
"withdrawal_id", "withdraw_id", "wopid", "id",
"transactionid", "transaction_id",
):
ctx["id"] = v
if kl in ("amount", "rawamount", "instructedamount") and isinstance(v, str):
elif kl in (
"amount", "rawamount", "instructedamount",
"amounteffective", "amountraw", "transferamount",
):
ctx["amount"] = v
if kl in ("taler_withdraw_uri", "talerwithdrawuri", "uri") and isinstance(v, str):
elif kl in (
"taler_withdraw_uri", "talerwithdrawuri", "uri",
"bankconfirmationurl", "confirmtransferurl",
):
ctx["uri"] = v
for k, v in o.items():
kl = str(k).lower().replace("-", "_")
if kl in ("reserve_pub", "reservepub") and isinstance(v, str) and len(v) >= 40:
bag.append((v, dict(ctx)))
walk_collect(v, bag, ctx)
elif not isinstance(v, (str, int, float, bool)) and v is not None:
walk_collect(v, bag, ctx)
elif isinstance(o, list):
for i in o:
walk_collect(i, bag, ctx)
raw_pubs = [] # ordered as found
scored = [] # (score, pub) higher better
blob_all = ""
for p in paths:
try:
blob_all += open(p, errors="replace").read() + "\n"
except Exception:
pass
def score_ctx(pub, ctx, window=""):
score = 0
cid = str(ctx.get("id") or "")
camt = str(ctx.get("amount") or "")
curi = str(ctx.get("uri") or "")
# bankConfirmationUrl / withdraw URI embeds this op's WID
if wid and wid in curi:
score += 120
if wid and wid in cid:
score += 100
if wid and wid in window:
score += 80
if amt and (camt == amt or camt.endswith(amt_num)):
score += 50
if amt and amt in window:
score += 30
if pub in used:
score -= 200
return score
# 1) full JSON objects in files
def iter_json_objects(text):
"""Yield top-level JSON objects from mixed log+JSON wallet dumps."""
i, n = 0, len(text)
while i < n:
if text[i] == "{":
try:
o, end = dec.raw_decode(text, i)
yield o
i = end
continue
except Exception:
pass
i += 1
raw_pubs = []
scored = []
# 1) structured JSON via raw_decode (accept + transactions)
for p in paths:
try:
t = open(p, errors="replace").read()
except Exception:
continue
# whole-file JSON
for m in re.finditer(r"\{", t):
try:
o = json.loads(t[m.start():])
except Exception:
continue
for o in iter_json_objects(t):
bag = []
walk_collect(o, bag)
for pub, ctx in bag:
raw_pubs.append(pub)
score = 0
cid = str(ctx.get("id") or "")
camt = str(ctx.get("amount") or "")
curi = str(ctx.get("uri") or "")
if wid and wid in cid:
score += 100
if wid and wid in curi:
score += 80
if amt and (camt == amt or camt.endswith(amt.split(":", 1)[-1])):
score += 40
if pub in used:
score -= 200
scored.append((score, pub))
scored.append((score_ctx(pub, ctx), pub))
# 2) regex fallback on accept file only (more current)
try:
acc = open(paths[0], errors="replace").read()
except Exception:
acc = ""
for pat in (
r"\"reserve_pub\"\s*:\s*\"([A-Z0-9]{40,})\"",
# 2) regex on *both* accept and tx (reservePub is normally only in transactions)
RPUB_PATS = (
r"\"reservePub\"\s*:\s*\"([A-Z0-9]{40,})\"",
r"reserve_pub[\"\s:=]+([A-Z0-9]{40,})",
):
for m in re.finditer(pat, acc, re.I):
pub = m.group(1)
raw_pubs.append(pub)
score = 10
# proximity to WID in accept output
window = acc[max(0, m.start() - 400) : m.end() + 400]
if wid and wid in window:
score += 100
if amt and amt in window:
score += 30
if pub in used:
score -= 200
scored.append((score, pub))
r"\"reserve_pub\"\s*:\s*\"([A-Z0-9]{40,})\"",
r"reserve[_ ]?pub[\"\s:=]+([A-Z0-9]{40,})",
)
for p in paths:
try:
blob = open(p, errors="replace").read()
except Exception:
continue
for pat in RPUB_PATS:
for m in re.finditer(pat, blob, re.I):
pub = m.group(1)
raw_pubs.append(pub)
window = blob[max(0, m.start() - 600) : m.end() + 600]
scored.append((score_ctx(pub, {}, window) + 10, pub))
# prefer high score, then later occurrence
order = []
seen = set()
for score, pub in sorted(scored, key=lambda x: (-x[0],), reverse=False):
# sort by score desc: use reverse sorted
pass
for score, pub in sorted(scored, key=lambda x: x[0], reverse=True):
if pub in seen or pub in used:
continue
seen.add(pub)
order.append(pub)
# append unused raw in reverse (newest-ish)
for pub in reversed(raw_pubs):
if pub in seen or pub in used:
continue