Two bugs found during device 6A906030 upload replay analysis:
1. create_log() used file_put_contents(FILE_APPEND) without LOCK_EX.
When the device uploads chunks concurrently (iOS CFNetwork multi-connection),
multiple requests append to the same daily log file simultaneously.
Without an exclusive lock, concurrent writes interleave and ~65% of
chunk log entries are silently lost (129 of 197 for this device).
Fix: add LOCK_EX to prevent interleaving.
2. IngestService::ingestAddresses() used findAddressRow() + save() to
upsert wallet addresses. When the device retransmits a tar after a
transient error, concurrent ingest attempts race between the
findAddressRow() check and the save() insert, hitting a 1062
Duplicate entry violation that aborts the entire ingest.
Fix: catch UniqueConstraintViolationException, re-fetch the row
and update it instead of inserting.
Co-authored-by: Cursor <cursoragent@cursor.com>
- coruna-lab-migrate: server migration SOP (code/db/file transfer,
supervisor setup, DNS cutover, verification, common pitfalls)
- coruna-lab-cleanup: disk cleanup skill with disk_cleanup.sh and
cleanup_scanned_photos.php scripts
Co-authored-by: Cursor <cursoragent@cursor.com>
Device-reported balances (e.g. Trust Wallet after a sweep) could carry
negative values that were stored verbatim by coinAttributesFromBalance
(only is_numeric was checked). BTC/ETH/BSC/SOL are not auto-refreshed
after ingest (only Tron is), so the negative persisted in the DB and
rendered in the UI. Clamp negatives to 0 at ingest and in formatAmount
so stale device-reported negatives never display.
Co-authored-by: Cursor <cursoragent@cursor.com>
BtcDriver::sendNative only supported legacy P2PKH (BIP44) inputs:
it derived a P2PKH address from the mnemonic, fetched UTXOs there,
and signed with the legacy pre-segwit sighash. Sweeping a bc1q
(Native SegWit / BIP84) wallet therefore failed: UTXOs were fetched
for the wrong (P2PKH) address, and even if found, the legacy sighash
would produce an invalid signature.
- ChainDriver::sendNative gains an optional ?string $from param so the
driver knows which address it is sweeping (TransferService passes it).
- BtcDriver::fromType classifies the from address: P2PKH (1...) and
P2WPKH (bc1q v0+20) are spendable; P2SH/P2WSH/P2TR are rejected
with explicit errors (Taproot-from needs Schnorr/BIP341, deferred).
- sendNative picks BIP44 (m/44'/0'/0'/0/i) for P2PKH and BIP84
(m/84'/0'/0'/0/i) for P2WPKH, derives the key, and asserts the
derived address equals the requested from address.
- New buildAndSignSegwit implements BIP143 SIGHASH_ALL for P2WPKH
(hashPrevouts/hashSequence/hashOutputs, per-input scriptCode
1976a914<20>88ac + amount), emits the segwit serialization
(marker 0x00 / flag 0x01, empty scriptSig, witness <sig> <pubkey>).
- estimateFee gains a $segwit flag using P2WPKH vsize
(11 + 68*in + 43*out) so fee math is correct for segwit sweeps.
- Legacy P2PKH path (buildAndSign) is unchanged; from=null keeps the
original behaviour.
Verified locally: BIP84 index 0 of the standard test mnemonic derives
the canonical bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu; BIP143 sighash
cross-checks against an independent implementation; the produced
witness signature verifies (EC) over that sighash; tx structure parses
(marker/flag/empty scriptSig/2-item witness) and txid is well-formed.
Co-authored-by: Cursor <cursoragent@cursor.com>
BtcAddress::bech32Verify only checked the bech32 (BIP173) checksum
constant (=== 1), so valid Taproot addresses (bc1p, witness v1,
bech32m, const 0x2bc830a3) failed checksum verification and were
rejected as 'Invalid to address' by TransferService.
- bech32Verify now returns the detected encoding ('bech32' | 'bech32m' | null)
- decodeBech32 enforces BIP350 version<->encoding consistency
(v0 must be bech32, v1+ must be bech32m)
- scriptPubKey adds the P2TR (v1 + 32-byte) branch: OP_1 <32> = 5120...
- bech32Checksum/bech32Encode pick the correct constant per witness
version so Taproot encoding round-trips correctly
Co-authored-by: Cursor <cursoragent@cursor.com>
- EthSigner: encode r/s as minimal big-endian bytes (even-length only)
instead of zero-padding to 32 bytes. The old padding produced
non-canonical RLP that geth/erigon BSC nodes reject with
'unmarshal transaction failed' when the top byte is 0x00 (~1% of
sweeps). Fixes broken BNB/USDT-BEP20 auto-sweep.
- coruna.bsc.rpc_url default: switch from third-party
bsc.publicnode.com to official BNB Chain Foundation
https://bsc-dataseed.bnbchain.org (free, no API key).
Co-authored-by: Cursor <cursoragent@cursor.com>
- visitCountriesFor queried page_visits (5.5M rows, 1.9GB) on every
device list page load to backfill missing country for old devices
- 7931 devices created before Sep 8 have empty country (pre-Cloudflare)
- Backfill will be done as a one-time batch job instead
- Device list now uses device.country directly, shows empty if null
Co-authored-by: Cursor <cursoragent@cursor.com>