441 lines
49 KiB
JavaScript
441 lines
49 KiB
JavaScript
// clasifica.mjs (quantum exposure report, C12): the CLASSIFICATION of one address from what was (or was not) observed. Exactly two
|
|
// labels (limbaj.mjs) plus the state "unmeasured". Every verdict names the evidence that WAS seen and what was NOT seen, in a fixed
|
|
// vocabulary, so that a reader knows what the label rests on. Nothing here reads a chain: the adapters (adaptoare.mjs) bring
|
|
// observations, mode 1 brings none.
|
|
//
|
|
// What exposes a public key on chain (project document, sections 2.2 N1/N8 and 1.2):
|
|
// Bitcoin: P2PK and bare multisig outputs carry the key(s) in the script; P2TR outputs carry the output (tweaked) key; a spend of a
|
|
// hash-type output (P2PKH, P2WPKH, P2SH, P2WSH) reveals the key or the script in the scriptSig / witness.
|
|
// Ethereum: an externally owned account reveals its key with the first transaction it sends (the signature) or with an EIP-7702
|
|
// authorization it signs (the delegation designator 0xef0100 || address in its code is set only after such an
|
|
// authorization was included); a contract account has no key of its own bound to the address.
|
|
// What this tool cannot see from a chain: keys disclosed OFF chain (shared xpubs, signed messages, PSBTs, EIP-191/712 signatures);
|
|
// those are asked as DECLARATIONS in the report (raport.mjs) and never turn into a label.
|
|
// What never becomes "no on-chain evidence of exposure": ANYTHING read through Bitcoin Core (it keeps no address index, so the history
|
|
// it shows of an address is the --txids list the requester gave, which no answer of the node shows to be complete: an address is
|
|
// "exposed" there, or "unmeasured" with the most specific reason: no list, a list none of whose CONFIRMED transactions touches the
|
|
// address, a spent output whose spending transaction was not given, or a list that cannot be shown complete), an index that counts more
|
|
// spent outputs than the pages served, a spend seen only in the target's mempool (or counted there by the index and not served), a
|
|
// P2TR / P2PK / bare-multisig output seen only in the target's mempool, a spend whose revealed script this tool does not classify, a
|
|
// spend SEEN of a hash-type output in which the tool reads no key or script (a valid spend reveals one; "seen" = it spends an output
|
|
// paying the script, or a push or script in it hashes to the address), an empty or erroneous answer, a target whose tip header time is
|
|
// more than MARJA_VARF_ORE hours before the reference instant (it is behind the chain: a later spend is not in its history), a target
|
|
// whose tip was not read, a SIBLING script (the same HASH160 of the key as P2PKH, P2WPKH or P2SH-P2WPKH; for a P2PK input also the same
|
|
// key in its other encodings and its BIP-86 P2TR output) that could not be measured, an index that counts more outputs or transactions
|
|
// of the script in its mempool than its page carried, or that served no mempool counts that are counts, an index that counts more
|
|
// outputs to the script on chain than its pages carried, an Ethereum endpoint whose head is more than MARJA_CAP_ETH_MIN minutes before
|
|
// the reference instant or that reports that it is syncing, an Ethereum account whose nonce and code were not proven under the state root
|
|
// of that head (adaptoare.mjs stareLaCap: no eth_getProof, a proof that does not lead to that root, a code that does not hash to the code
|
|
// hash proven; each is "unmeasured" there).
|
|
// And no label at all (exposed or not) for: a target that serves ANOTHER CHAIN than the address's network (its genesis block, or a tip
|
|
// header above the proof-of-work limit of the chain that genesis names, or, on mainnet, another block than the checkpoint at the highest
|
|
// checkpoint height at or below the end of its chain, or a chain that ends below the lowest checkpoint; a p2pk: or script: line is read
|
|
// on mainnet only), and a target that serves blocks AFTER the report date (its tip, an Ethereum head, a given transaction's block, or for
|
|
// Bitcoin Core the scan, read at the run, more than MARJA_VARF_ORE hours after the reference instant): the report records what was
|
|
// readable on its date.
|
|
// Each of those is "unmeasured", with the reason. A spend in the mempool never REMOVES evidence that is already on chain (a P2TR / P2PK /
|
|
// bare-multisig output stays "exposed"); a sibling script that shows the key on chain makes the input "exposed", with the sibling named.
|
|
import { ETICHETA, NEMASURAT } from './limbaj.mjs';
|
|
import { DOVEZI_REVELATOARE as REVELATOARE, RETELE_BTC, reteleIntrarii, tintaDinBits } from './bitcoin.mjs';
|
|
import { esteDelegare7702, delegatDin7702 } from './ethereum.mjs';
|
|
|
|
const TIP_EXPUS = Object.freeze({
|
|
p2pk: 'P2PK output: the public key is the script',
|
|
p2ms: 'bare multisig output: the public keys are the script',
|
|
p2tr: 'P2TR output: the output key (the tweaked public key) is the script',
|
|
});
|
|
const TIP_HASH = Object.freeze({
|
|
p2pkh: 'P2PKH: the output carries HASH160 of the key; the key is revealed by a spend',
|
|
p2wpkh: 'P2WPKH: the output carries HASH160 of the key; the key is revealed by a spend',
|
|
p2sh: 'P2SH: the output carries HASH160 of a script; the script (and the keys in it) is revealed by a spend',
|
|
p2wsh: 'P2WSH: the output carries SHA-256 of a script; the script (and the keys in it) is revealed by a spend',
|
|
});
|
|
const NUME_TIP = Object.freeze({ p2pk: 'P2PK', p2pkh: 'P2PKH', p2sh: 'P2SH', p2ms: 'bare multisig', p2wpkh: 'P2WPKH', p2wsh: 'P2WSH', p2tr: 'P2TR', op_return: 'OP_RETURN', nonstandard: 'non-standard', 'witness-unknown': 'witness program (unknown version)' });
|
|
export const numeTip = (t) => NUME_TIP[t] || t;
|
|
const OFF_CHAIN = 'keys disclosed off chain (shared extended public keys, signed messages, PSBTs, a spend broadcast and never confirmed: replaced or dropped from mempools): not visible from any chain; see the declarations';
|
|
/** where an output or a spend sits: the height when known, else the block hash, whole (Bitcoin Core verbose transactions carry no height,
|
|
* and a block hash starts with about nineteen zero digits, so a prefix names nothing) */
|
|
const unde = (o) => (Number.isInteger(o.height) ? `height ${o.height}` : o.block ? `block ${String(o.block)}` : 'position not served');
|
|
|
|
const intrare = (t) => ({ input: t.input, chain: 'bitcoin', network: t.network, scriptType: t.type, scriptTypeName: numeTip(t.type), form: t.form });
|
|
const nem = (r, motiv) => ({ ...r, label: null, unmeasured: motiv });
|
|
|
|
/** mode 1, Bitcoin: by script type only */
|
|
export function clasificaBitcoinOffline(t) {
|
|
const r = { ...intrare(t), mode: 1, evidence: [], notSeen: [], label: null, unmeasured: null };
|
|
if (t.error) return nem(r, 'not classified: ' + t.error);
|
|
if (TIP_EXPUS[t.type]) {
|
|
r.label = ETICHETA.EXPUSA;
|
|
r.evidence.push('by script type: ' + TIP_EXPUS[t.type] + (t.type === 'p2ms' ? ` (${t.m}-of-${t.n})` : ''));
|
|
r.notSeen.push('whether any output of this script exists on chain (mode 1 reads no chain data; the key is on chain from the first such output)');
|
|
} else if (TIP_HASH[t.type]) {
|
|
r.label = ETICHETA.FARA_DOVADA;
|
|
r.evidence.push('by script type: ' + TIP_HASH[t.type]);
|
|
r.notSeen.push('spends from this address (mode 1 reads no chain data; a single spend would reveal the key or the script)');
|
|
r.notSeen.push('whether the address has ever been used');
|
|
r.notSeen.push(linieN8(t, 1));
|
|
r.notSeen.push(ALTE_LANTURI_BTC(1));
|
|
} else if (t.type === 'op_return') return nem(r, 'OP_RETURN script: not a spendable output, nothing to classify');
|
|
else return nem(r, `script type "${t.type}" is not classified by this tool`);
|
|
return r;
|
|
}
|
|
|
|
/** the margin within which the header time of a target's tip must fall, before the reference instant of the report, for "no on-chain
|
|
* evidence of exposure" to be given (README, "The tip of the target"): blocks arrive every 10 minutes on average, and a header's time,
|
|
* set by its miner, only has to exceed the median of the 11 previous blocks, so it may trail real time by about an hour; two hours keep
|
|
* a target that has caught up measurable, and leave a target about 12 or more blocks behind unmeasured */
|
|
export const MARJA_VARF_ORE = 2;
|
|
/** the margin within which the time of an Ethereum endpoint's HEAD block must fall, before the reference instant, for "no on-chain evidence
|
|
* of exposure" to be given (README, "The head of an Ethereum endpoint"): a block every 12-second slot, so an endpoint that has caught up
|
|
* serves a head seconds old; 30 minutes (150 slots) leave room for a skewed clock of the machine that runs the report and for a run of
|
|
* missed slots, and leave an endpoint about 150 or more blocks behind unmeasured */
|
|
export const MARJA_CAP_ETH_MIN = 30;
|
|
const isoS = (s) => new Date(s * 1000).toISOString().replace('.000Z', 'Z');
|
|
const NUME_SORA = Object.freeze({ p2pkh: 'P2PKH', p2wpkh: 'P2WPKH', 'p2sh-p2wpkh': 'P2SH-P2WPKH', p2pk: 'P2PK', p2tr: 'P2TR' });
|
|
/** how a sibling relates to the input (bitcoin.mjs scripturiSurori): the words that open its evidence line, and the reason a sibling that
|
|
* could not be measured leaves the input unmeasured */
|
|
const RELATIE = Object.freeze({
|
|
hash160: (s) => `the same HASH160 of the key under another script type, ${s.nume}`,
|
|
codificare: (s) => `the same key in its ${s.codificare} encoding, under ${s.nume}`,
|
|
bip86: (s) => `a P2TR output with no script tree (BIP-86) whose internal key is this key, ${s.nume}`,
|
|
});
|
|
const DE_CE_SORA = Object.freeze({
|
|
hash160: 'the same HASH160 of the key under another script type, where a spend reveals this same key',
|
|
codificare: 'the same key in another encoding, where an output or a spend shows this same key',
|
|
bip86: 'the BIP-86 P2TR output of this key, whose output key is this key tweaked by a hash of it',
|
|
});
|
|
const relatia = (s) => (RELATIE[s.relatie] || RELATIE.hash160)(s);
|
|
/** what the same key may sit under and this tool cannot derive from the input (project document N8), per input type */
|
|
const N8_NEDERIVAT = Object.freeze({
|
|
p2pkh: 'a P2PK or bare-multisig output holding the key itself, a P2TR output or script, a P2SH or P2WSH script holding the key, and the key in its other encoding (compressed or uncompressed, which has another HASH160)',
|
|
p2wpkh: 'a P2PK or bare-multisig output holding the key itself, a P2TR output or script, a P2SH or P2WSH script holding the key, and the key in its other encoding (compressed or uncompressed, which has another HASH160)',
|
|
p2pk: 'a P2TR output whose key commits to a script tree, a P2SH or P2WSH script holding the key, and a bare-multisig output holding it: each depends on a tree, a script or other keys that this input does not give (the few forms that commit to this key alone, such as a 1-of-1 multisig or a P2WSH of <key> OP_CHECKSIG, are not searched either)',
|
|
p2sh: 'the keys inside the script (unknown until a spend reveals it) under any other script type',
|
|
p2wsh: 'the keys inside the script (unknown until a spend reveals it) under any other script type',
|
|
p2tr: 'the internal key or the output key under any other script type (a P2PK, P2PKH or P2WPKH of the same key)',
|
|
p2ms: 'each key of this multisig under any other script type (a P2PK, P2PKH or P2WPKH of the same key)',
|
|
});
|
|
/** N8: the same key under another script type. mode 1 reads nothing; in modes 2 and 3 the derivable siblings were read (named in the
|
|
* evidence), and this line names what cannot be derived */
|
|
const linieN8 = (t, mode) => (mode === 1
|
|
? `the same key under another script type (${t.type === 'p2pk' ? 'the key in each of its encodings under P2PK, P2PKH, P2WPKH and P2SH-P2WPKH, its BIP-86 P2TR output, and ' : ['p2pkh', 'p2wpkh'].includes(t.type) ? 'the other P2PKH, P2WPKH or P2SH-P2WPKH forms of the same HASH160 of the key, and ' : ''}${N8_NEDERIVAT[t.type] || 'any other form'}): mode 1 reads no chain data`
|
|
: `the same key under another script type that cannot be derived from this input: ${N8_NEDERIVAT[t.type] || 'any other form'}; not searched (a key reused there is revealed by that script's own outputs or spends)`);
|
|
/**
|
|
* A. the CHAIN a Bitcoin target serves (Electrum server.features genesis_hash; Esplora /block-height/0; Bitcoin Core getblockhash 0),
|
|
* judged before any label: its genesis block must be one of the networks the input can belong to (bitcoin.mjs reteleIntrarii; a p2pk: or
|
|
* script: line is read on mainnet only); the tip header it serves (Electrum, Esplora) must state a target within the proof-of-work limit of
|
|
* the chain that genesis names (a signet or regtest header under a mainnet genesis states one above it); and on a network that holds
|
|
* CHECKPOINTS (mainnet: bitcoin.mjs punctDeControl) the block it serves at the highest checkpoint height at or below its chain's end must
|
|
* be the checkpoint: a chain that split from Bitcoin after its genesis block (Bitcoin Cash, Bitcoin SV, eCash) shares that block and the
|
|
* limit, and serves another block there. -> { linie } or { linie, motiv }; null when the chain was not read
|
|
*/
|
|
const DESPARTITE = 'Bitcoin Cash, Bitcoin SV and eCash share the genesis block and the proof-of-work limit of Bitcoin mainnet; Bitcoin Cash split from it at height 478559, and the other two split later from Bitcoin Cash';
|
|
function lantulTintei(t, obs) {
|
|
if (!obs.chain) return null;
|
|
const retea = obs.chain.retea; const R = retea ? RETELE_BTC[retea] : null;
|
|
const linie = R ? `chain served: ${R.nume} (genesis block ${R.geneza})` : `chain served: genesis block ${obs.chain.geneza}, which is none of Bitcoin mainnet, testnet3, testnet4, signet or regtest`;
|
|
const permise = reteleIntrarii(t);
|
|
const aAdresei = t.network === 'mainnet' || t.network === 'testnet' || t.network === 'regtest' ? `this is a ${t.network} address (${permise.map((n) => RETELE_BTC[n].nume).join(', ')})` : `this is a ${t.form === 'p2pk' ? 'p2pk:' : 'script:'} line, which this tool reads on Bitcoin mainnet only`;
|
|
if (!R || !permise.includes(retea)) return { linie, motiv: `not measured: the target serves another chain: ${R ? R.nume : 'a chain whose genesis block is ' + obs.chain.geneza}, and ${aAdresei}; nothing it serves is about this address's chain` };
|
|
// a tip whose bits were not read states no target, and is not let through as one within the limit
|
|
const gen = `genesis block ${R.geneza}, that of ${R.nume}`;
|
|
const tintaVarf = obs.tip ? tintaDinBits(Number(obs.tip.bits) >>> 0) : null;
|
|
if (obs.tip && (tintaVarf === null || tintaVarf > R.limita)) return { linie: `chain served: ${gen}, with a tip header that no header of ${R.nume} can carry`, motiv: `not measured: the target serves another chain: the tip header it serves (height ${obs.tip.height}, bits 0x${(Number(obs.tip.bits) >>> 0).toString(16).padStart(8, '0')}) states ${tintaVarf === null ? 'no valid proof-of-work target' : 'a proof-of-work target above the limit of ' + R.nume}, which no header of that chain can carry` };
|
|
const limita = obs.tip ? '; the tip header states a target within its proof-of-work limit' : '';
|
|
const munca = 'whether it is the chain with the most work is not verified';
|
|
if (!(R.puncte && R.puncte.length)) return { linie: `chain served: ${R.nume} (genesis block ${R.geneza}${limita}; no checkpoint is held for ${R.nume}, so a chain that shares its genesis block is not told apart; ${munca})` };
|
|
// the checkpoint: a chain that shares the genesis block and the limit, and split later, serves another block at that height
|
|
const c = obs.chain.control;
|
|
if (!c) return { linie: `chain served: ${gen}; its block at a checkpoint height was not read`, motiv: `not measured: the target serves the genesis block of ${R.nume}, but its block at a checkpoint height was not read, so a chain that split from ${R.nume} after its genesis block cannot be told apart from it (${DESPARTITE})` };
|
|
if (c.sub !== undefined) return { linie: `chain served: ${gen}, ending at height ${c.inaltime}, below the lowest checkpoint this tool holds (height ${c.sub})`, motiv: `not measured: the target serves the genesis block of ${R.nume}, and the chain it serves ends at height ${c.inaltime}, below height ${c.sub}, the lowest checkpoint this tool holds for ${R.nume}: a chain that split from it there cannot be told apart (${DESPARTITE})` };
|
|
if (c.servit !== c.asteptat) return { linie: `chain served: ${gen}, but at height ${c.height} the block ${c.servit}, where ${R.nume} has the block ${c.asteptat}`, motiv: `not measured: the target serves another chain: one that shares the genesis block of ${R.nume} and split from it before height ${c.height}, where it serves the block ${c.servit} and ${R.nume} has the block ${c.asteptat} (${DESPARTITE}); nothing it serves is about this address's chain` };
|
|
return { linie: `chain served: ${R.nume} (genesis block ${R.geneza}; at height ${c.height} the block ${c.servit}, the checkpoint this tool holds${limita}; ${munca})` };
|
|
}
|
|
/**
|
|
* C. the NEWEST chain time a target served (the header time of its tip, the time of an Ethereum head, the block time of a given
|
|
* transaction, and for Bitcoin Core the scan itself, which reads the node's unspent set at the time of the run) may not be more than
|
|
* MARJA_VARF_ORE hours after the reference instant: the report records what was readable on its date, and a block that came after it was
|
|
* not. Mainnet nodes refuse a header more than 2 hours ahead of their own time, so a target that serves the chain passes this for a report
|
|
* dated the day it is run. -> the reason, or null
|
|
*/
|
|
function dupaDataRaportului(timp, ce, ctx, cine = 'the target') {
|
|
if (!Number.isFinite(ctx.referinta)) return `not measured: ${ce} ${isoS(timp)}, but no reference instant was given to judge it against`;
|
|
if (timp * 1000 > ctx.referinta + MARJA_VARF_ORE * 3600 * 1000) return `not measured: ${cine} serves blocks after the report date: ${ce} ${isoS(timp)}, more than ${MARJA_VARF_ORE} hours after ${ctx.referintaText || 'the reference instant'}; this report records what was readable on its date and cannot rest on what came after it; date the report the day it is run`;
|
|
return null;
|
|
}
|
|
/** MIC-1: a key, or an address, shared with another chain is revealed by a signature there; only the chain named was read */
|
|
const ALTE_LANTURI_BTC = (mode) => `other chains sharing this key: a Bitcoin fork that shares this chain's history before its split, a test network, or a chain of another kind using the same secp256k1 key (an Ethereum account of the same key): ${mode === 1 ? 'no chain was read (mode 1)' : 'only the Bitcoin chain served by the target was read'}; see the declarations`;
|
|
const ALTE_LANTURI_ETH = (chainId) => `other chains sharing this key: every EVM chain where this address is the same account (layer 2s, sidechains, test networks), and Bitcoin addresses of the same secp256k1 key: ${chainId === undefined || chainId === null ? 'no chain was read (mode 1)' : `only chain id ${chainId} was read`}; see the declarations`;
|
|
const OFF_CHAIN_ETH = 'keys disclosed off chain (EIP-191 and EIP-712 signatures: token permits, exchange orders, sign-in messages; a transaction broadcast and never included; an extended public key of the wallet given to a third party): not visible from any chain; see the declarations';
|
|
/** the revealing findings of an observation, as evidence lines */
|
|
const liniiRevelatoare = (obs) => (obs.inputs || []).filter((i) => i.findings.some((f) => REVELATOARE.has(f.what))).flatMap((i) => i.findings.filter((f) => REVELATOARE.has(f.what)).map((f) => `${f.detail}, in the spend ${i.txid} (input ${i.index}, ${unde(i)})`));
|
|
|
|
/**
|
|
* mode 2/3, Bitcoin: from observations (adaptoare.mjs, observatieDinTranzactii), then the SIBLING scripts and the TIP of the target
|
|
* obs = { outputs: [{txid, vout, height}], inputs: [{txid, index, height, findings: [{what, detail}], spendsTarget}], spentOutputs, txCount,
|
|
* unconfirmedCount, unconfirmedRevealing, unconfirmedOutputs, unconfirmedInputs, unconfirmedSpends, spendsNotEnumerable, txidsGiven,
|
|
* mempoolTouches, historyFromRequesterList, unspentFound, spentAbsent, spentNotRead, spentOutputsServed, mempoolSpendsCounted,
|
|
* mempoolSpendNotServed, note, partial, error, tip: { height, time, hash }, siblings: [{ type, input, t, obs }] }
|
|
* ctx = { referinta (ms), referintaText }: the instant the tip is judged against (raport.mjs: the end of the report's date, or the time of
|
|
* the run when that comes first)
|
|
* Order: the chain the target serves (A, lantulTintei) and the newest time it served (C, dupaDataRaportului), before any label; then the
|
|
* input itself (clasificaUna); a sibling that shows the key on chain makes it "exposed"; a sibling that could not be measured leaves no
|
|
* "no on-chain evidence"; a target whose tip header is older than the margin leaves no "no on-chain evidence"; every remaining "no
|
|
* on-chain evidence" names the siblings read, what cannot be derived (N8) and the other chains not read
|
|
*/
|
|
export function clasificaBitcoinObservat(t, obs, mode, ctx = {}) {
|
|
let r = clasificaUna(t, obs, mode);
|
|
if (t.error || !obs || obs.error || obs.partial) return r;
|
|
// A. the chain the target serves (Electrum, Esplora): nothing a target serves about another chain is about this address, so no label
|
|
const lant = lantulTintei(t, obs);
|
|
const linieVarf = obs.tip ? `history served up to height ${obs.tip.height} (tip header time ${isoS(obs.tip.time)})` : null;
|
|
const numaiTinta = (motiv) => ({ ...r, label: null, unmeasured: motiv, evidence: [linieVarf, lant && lant.linie].filter(Boolean), notSeen: [] });
|
|
if (lant && lant.motiv) return numaiTinta(lant.motiv);
|
|
// C. nothing the target served may come after the report date (Electrum, Esplora: the tip; Bitcoin Core: the scan, read at the run, and
|
|
// the block of each given transaction), whatever the label would be
|
|
const dupa = obs.tip ? dupaDataRaportului(obs.tip.time, `its tip ${obs.tip.height} carries the header time`, ctx) : obs.timpMaxim ? dupaDataRaportului(obs.timpMaxim.time, obs.timpMaxim.ce, ctx) : null;
|
|
if (dupa) return numaiTinta(dupa);
|
|
if (obs.blocktimeLipsa) return numaiTinta(`not measured: the node served transaction ${obs.blocktimeLipsa} (given with --txids) in a block of its active chain without the time of that block (blocktime), so it cannot be placed against the report date`);
|
|
// the siblings: the same HASH160 under P2PKH / P2WPKH / P2SH-P2WPKH, and for a P2PK input the same key in its other encodings and its
|
|
// BIP-86 P2TR output (bitcoin.mjs scripturiSurori)
|
|
const surori = (Array.isArray(obs.siblings) ? obs.siblings : []).map((s) => ({ ...s, nume: `${NUME_SORA[s.type] || s.type} ${s.input}`, r: clasificaUna(s.t, s.obs, mode) }));
|
|
const expusa = surori.filter((s) => s.r.label === ETICHETA.EXPUSA);
|
|
if (expusa.length) {
|
|
const fost = r.unmeasured;
|
|
r = { ...r, label: ETICHETA.EXPUSA, unmeasured: null, evidence: [...r.evidence], notSeen: [...r.notSeen] };
|
|
for (const s of expusa) r.evidence.push(`${relatia(s)}: ${liniiRevelatoare(s.obs).join('; ') || s.r.evidence.join('; ')}`);
|
|
if (fost) r.notSeen.push('this input itself was not measured (' + fost.replace(/^not measured: /, '') + '); the key is on chain through the script named in the evidence');
|
|
// a BIP-86 output shows the output key, this key tweaked: it is said exactly what that exposes
|
|
if (expusa.some((s) => s.relatie === 'bip86')) r.notSeen.push('the BIP-86 P2TR output named carries the output key Q = P + t*G (P this key, t a hash of it), not this key itself: whoever recovers the private key of Q controls that output, and the private key of this key follows from it for anyone who also knows this key');
|
|
} else if (r.label === ETICHETA.FARA_DOVADA) {
|
|
const nem_ = surori.find((s) => s.r.unmeasured);
|
|
if (nem_) r = nem(r, `not measured: the sibling script ${nem_.nume} (${DE_CE_SORA[nem_.relatie] || DE_CE_SORA.hash160}) was not measured: ${String(nem_.r.unmeasured).replace(/^not measured: /, '')}`);
|
|
else if (surori.length) r.evidence.push(t.type === 'p2pk'
|
|
? `sibling scripts of the same key read through the same target (the key in each of its encodings under P2PK, P2PKH, P2WPKH and P2SH-P2WPKH, and its BIP-86 P2TR output): ${surori.map((s) => `${s.nume} (${s.obs.txCount ?? 0} confirmed transaction(s))`).join(', ')}; none shows the key on chain`
|
|
: `sibling scripts of the same HASH160 read through the same target: ${surori.map((s) => `${s.nume} (${s.obs.txCount ?? 0} confirmed transaction(s))`).join(', ')}; no spend there reveals the key`);
|
|
}
|
|
// the tip: every verdict names up to where the history goes, and the chain served; "no on-chain evidence" only for a target that has
|
|
// caught up with the chain
|
|
if (obs.tip) {
|
|
const H = obs.tip.height, T = isoS(obs.tip.time);
|
|
r.evidence.unshift(`history served up to height ${H} (tip header time ${T})`);
|
|
if (lant && lant.linie) r.evidence.splice(1, 0, lant.linie);
|
|
if (r.label === ETICHETA.FARA_DOVADA) {
|
|
if (!Number.isFinite(ctx.referinta)) r = nem(r, `not measured: target tip ${H} at ${T} was read, but no reference instant was given to judge it against`);
|
|
else if (obs.tip.time * 1000 < ctx.referinta - MARJA_VARF_ORE * 3600 * 1000) r = nem(r, `not measured: target behind the chain: tip ${H} at ${T}, more than ${MARJA_VARF_ORE} hours before ${ctx.referintaText || 'the reference instant'}; a spend confirmed after its tip is not in the history it served; re-run against a target that has caught up with the chain`);
|
|
}
|
|
} else if (r.label === ETICHETA.FARA_DOVADA) r = nem(r, 'not measured: the tip of the target was not read, so the history it served cannot be placed against the chain');
|
|
// Bitcoin Core reads no tip: its verdicts name the chain the node serves as their first evidence line
|
|
if (!obs.tip && lant && lant.linie) r.evidence.unshift(lant.linie);
|
|
if (r.label === ETICHETA.FARA_DOVADA) { r.notSeen.push(linieN8(t, mode)); r.notSeen.push(ALTE_LANTURI_BTC(mode)); }
|
|
return r;
|
|
}
|
|
|
|
/** one script, from its own observation (the rules of the header comment) */
|
|
function clasificaUna(t, obs, mode) {
|
|
const r = { ...intrare(t), mode, evidence: [], notSeen: [], label: null, unmeasured: null };
|
|
if (t.error) return nem(r, 'not classified: ' + t.error);
|
|
if (!obs || obs.error) return nem(r, 'not measured: ' + (obs && obs.error ? obs.error : 'no observation'));
|
|
if (obs.partial) return nem(r, 'not measured: ' + obs.partial);
|
|
if (t.type === 'op_return') return nem(r, 'OP_RETURN script: not a spendable output, nothing to classify');
|
|
const tipExpus = !!TIP_EXPUS[t.type];
|
|
if (!tipExpus && !TIP_HASH[t.type]) return nem(r, `script type "${t.type}" is not classified by this tool`);
|
|
const outs = obs.outputs || [], ins = obs.inputs || [];
|
|
const areRevel = (i) => i.findings.some((f) => REVELATOARE.has(f.what));
|
|
const revel = ins.filter(areRevel);
|
|
const spends = ins.filter((i) => i.spendsTarget);
|
|
// a spend SEEN: an input that spends an output paying this script (its funding was read), or, for a hash-type script, an input in which
|
|
// a push or a script hashes to the address's hash (bitcoin.mjs emits a finding for a hash-type target only on such an equality): the
|
|
// preimage of the address is then on chain in that input, even when its funding was not served and a key in it was not read (a
|
|
// segwit spend served without its witness, which the transaction id does not cover)
|
|
const spendsVazute = ins.filter((i) => i.spendsTarget || (!tipExpus && i.findings.length > 0));
|
|
const neclasif = ins.filter((i) => !areRevel(i) && i.findings.some((f) => f.what === 'script-revealed-unclassified'));
|
|
const listaIesiri = (l) => l.slice(0, 3).map((o) => `${o.txid}:${o.vout} (${unde(o)})`).join(', ') + (l.length > 3 ? ', ...' : '');
|
|
// where a transaction that is NOT on chain sits: the target's mempool, or (Bitcoin Core) a block off the node's active chain
|
|
const neconf = obs.offChainBlocks > 0 ? "in the target's mempool or in a block off its active chain (confirmations 0)" : "in the target's mempool";
|
|
|
|
// Bitcoin Core without --txids, or with --txids none of which touches THIS address: scantxoutset enumerates only UNSPENT outputs; a
|
|
// spend (the thing that reveals a key) is invisible
|
|
if (obs.spendsNotEnumerable) {
|
|
// a list that touches the address only through transactions the node holds in its MEMPOOL says nothing about its history on chain
|
|
const prinMempool = obs.mempoolTouches > 0 ? ` (${obs.mempoolTouches} output(s) or input(s) of this address are in given transactions the node holds only in its mempool${obs.offChainBlocks > 0 ? ' or in a block off its active chain' : ''}, which are not on chain and say nothing about its history there)` : '';
|
|
const deCe = obs.txidsGiven ? `none of the ${obs.txidsGiven} transaction(s) given with --txids touches this address on chain (no confirmed output to it, no confirmed input spending it or revealing its key or script)${prinMempool}, and Bitcoin Core keeps no address index` : 'Bitcoin Core keeps no address index';
|
|
if (tipExpus && outs.length) {
|
|
r.label = ETICHETA.EXPUSA;
|
|
r.evidence.push(`${outs.length} unspent output(s) of this script found by scantxoutset: ` + listaIesiri(outs));
|
|
r.evidence.push(TIP_EXPUS[t.type] + (t.type === 'p2ms' ? ` (${t.m}-of-${t.n})` : '') + ': the key is on chain in each of those outputs');
|
|
r.notSeen.push(`spent outputs and spends: ${deCe}, so only unspent outputs were scanned; give this address's own transaction ids with --txids (from a node with txindex=1) to read them`);
|
|
r.notSeen.push(OFF_CHAIN);
|
|
return r;
|
|
}
|
|
if (tipExpus) return nem(r, `not measured: scantxoutset sees only unspent outputs and found none for this script; a spent output is invisible here (${deCe}); give this address's own transaction ids with --txids (from a node with txindex=1)`);
|
|
return nem(r, `not measured: spends not searched: ${deCe}, so a spend that revealed the key cannot be enumerated here; give this address's own transaction ids with --txids (from a node with txindex=1)`);
|
|
}
|
|
// the label a key-in-output type would get from its outputs alone: a spend in the mempool cannot take that evidence away
|
|
const expusPrinTip = tipExpus && outs.length > 0;
|
|
// a key-in-output type with no output on chain but one in the target's mempool: the key is public there, not yet on chain
|
|
if (tipExpus && !outs.length && obs.unconfirmedOutputs > 0) return nem(r, `not measured: ${obs.unconfirmedOutputs} output(s) of this script are ${neconf}, unconfirmed: a ${numeTip(t.type)} output carries its key, which is public there but not yet on chain; re-run after confirmation`);
|
|
// the same, when the index COUNTS an output of this script in its mempool that its transaction page did not carry
|
|
if (tipExpus && !outs.length && obs.mempoolOutputNotServed) return nem(r, `not measured: the target counts ${obs.mempoolOutputsCounted} output(s) of this script in its mempool (mempool_stats) and its transaction page carried ${obs.unconfirmedOutputs ?? 0} unconfirmed output(s) of it: a counted output was not served; a ${numeTip(t.type)} output carries its key, which is public there but not yet on chain; re-run`);
|
|
// a revealing spend seen only in the target's mempool is public but not on chain: neither label fits (unless the key is already on chain)
|
|
if (!revel.length && obs.unconfirmedRevealing && !expusPrinTip) return nem(r, `not measured: a spend of this address is ${neconf}, unconfirmed: the key is public there but not yet on chain; re-run after confirmation (${obs.unconfirmedCount} unconfirmed transaction(s) were not used as on-chain evidence)`);
|
|
// the index counts a spend of this address in its mempool that its transaction page did not carry (gone between the two requests)
|
|
if (!revel.length && !expusPrinTip && obs.mempoolSpendNotServed) return nem(r, `not measured: the target counts ${obs.mempoolSpendsCounted} spend(s) of this address in its mempool (mempool_stats) and its transaction page carried ${obs.unconfirmedSpends ?? 0} unconfirmed spend(s) of it: a counted spend was not served; a broadcast spend reveals the key even if it never confirms; re-run, and see the declarations`);
|
|
// the index counts more transactions of this script in its mempool than its page carried: its answer is incomplete, and what it did not
|
|
// serve may show the key
|
|
if (!revel.length && !expusPrinTip && obs.mempoolTxNotServed) return nem(r, `not measured: the target counts ${obs.mempoolTxsCounted} transaction(s) of this address in its mempool (mempool_stats) and its transaction page carried ${obs.unconfirmedCount ?? 0}: a counted transaction was not served, and what was not served may show the key; re-run`);
|
|
// the index served no mempool counts, or counts that are not counts: none of the three comparisons above could be made, so a spend, an
|
|
// output or a transaction it holds in its mempool and did not serve cannot be ruled out
|
|
if (!revel.length && !expusPrinTip && obs.mempoolNecitit) return nem(r, `not measured: the target served no mempool counts for this script that are counts (mempool_stats: ${obs.mempoolNecitit}), so a spend, an output or a transaction it holds in its mempool and did not serve cannot be ruled out; re-run`);
|
|
// a spend whose revealed script this tool does not classify: the key may be in it in a form the tool does not read
|
|
if (!revel.length && !tipExpus && neclasif.length) return nem(r, `not measured: ${neclasif.length} spend(s) of this address seen whose revealed script form is not classified by this tool: ` + neclasif[0].findings.find((f) => f.what === 'script-revealed-unclassified').detail);
|
|
// a spend SEEN of an output that pays this hash-type script: a valid spend reveals the preimage of the hash (the key, or the script), so
|
|
// a spend in which this tool finds neither has it in a form the tool does not read; "no on-chain evidence" would contradict the spend
|
|
if (!revel.length && !tipExpus && spendsVazute.length) return nem(r, `not measured: ${spendsVazute.length} spend(s) of this address seen (the first: ${spendsVazute[0].txid}, input ${spendsVazute[0].index}, ${unde(spendsVazute[0])}), each spending an output that pays this script or carrying a push or script that hashes to the address, which a valid spend does only by revealing the key or the script; in none of them did this tool read it (${spendsVazute[0].findings.length ? spendsVazute[0].findings.map((f) => f.detail).join('; ') : 'no push of the scriptSig or witness hashes to the address'}): it is in a form this tool does not read`);
|
|
// Bitcoin Core with --txids: an output of this address that the node's scan no longer finds unspent, and that none of the given
|
|
// transactions spends, was spent by a transaction NOT given: the key may be revealed there
|
|
if (!revel.length && !tipExpus && obs.spentNotRead > 0) return nem(r, `not measured: ${obs.spentNotRead} output(s) of this address are no longer unspent (scantxoutset), and the transaction(s) that spent them are not among the --txids given; the key may be revealed there; add them`);
|
|
// Esplora: the index counts more spent outputs than the spends read from the pages it served
|
|
if (!revel.length && !tipExpus && Number.isFinite(obs.spentOutputsServed) && obs.spentOutputsServed > spends.length) return nem(r, `not measured: the target counts ${obs.spentOutputsServed} spent output(s) of this address (chain_stats) but only ${spends.length} spend(s) were in the ${obs.txCount} transaction(s) it served; the key may be revealed in a spend not served`);
|
|
// Bitcoin Core: the history read here is the --txids list the requester gave, and no answer of the node shows it to be complete (it
|
|
// keeps no address index; an output of this address funded and spent by transactions not given is invisible to it). "No on-chain
|
|
// evidence of exposure" would rest on that list, not on a measurement: it is not given through this adapter, only "exposed" or this
|
|
if (obs.historyFromRequesterList && (tipExpus ? !outs.length : !revel.length)) return nem(r, `not measured: Bitcoin Core keeps no address index, so the history of this address read here is the --txids list given, and this tool cannot check that it lists every transaction of this address: the ${obs.txidsGiven ?? 0} transaction(s) given show no spend that reveals its key or script, and scantxoutset finds ${obs.unspentFound ?? 0} of its output(s) unspent, but an output funded and spent by transactions not given is invisible to the node; read the address through an index that serves its history (Electrum, Esplora) to measure it`);
|
|
// Esplora: the index counts more outputs to this script on chain than the outputs read from the pages it served: an output was not
|
|
// served (for a P2TR, P2PK or bare-multisig script it carries the key; for a hash type it may have a spend that reveals it)
|
|
if (!revel.length && !expusPrinTip && Number.isFinite(obs.fundedOutputsServed) && obs.fundedOutputsServed > outs.length) return nem(r, `not measured: the target counts ${obs.fundedOutputsServed} output(s) to this script on chain (chain_stats) but its pages carried ${outs.length}; an output not served ${tipExpus ? 'carries the key' : 'may have a spend that reveals the key'}`);
|
|
|
|
if (outs.length) r.evidence.push(`${outs.length} output(s) to this script on chain: ` + listaIesiri(outs));
|
|
else r.evidence.push('no output to this script in the history served by the target');
|
|
if (tipExpus) {
|
|
if (outs.length) { r.label = ETICHETA.EXPUSA; r.evidence.push(TIP_EXPUS[t.type] + (t.type === 'p2ms' ? ` (${t.m}-of-${t.n})` : '') + ': the key is on chain in each of those outputs'); }
|
|
else { r.label = ETICHETA.FARA_DOVADA; r.notSeen.push(`no output exists yet: a ${numeTip(t.type)} output exposes its key the moment it is created`); }
|
|
for (const i of spends) for (const f of i.findings) r.evidence.push(`spend in ${i.txid} (input ${i.index}, ${unde(i)}): ${f.detail}`);
|
|
} else {
|
|
if (revel.length) {
|
|
r.label = ETICHETA.EXPUSA;
|
|
for (const i of revel) for (const f of i.findings.filter((x) => REVELATOARE.has(x.what))) r.evidence.push(`${f.detail}, in the spend ${i.txid} (input ${i.index}, ${unde(i)})`);
|
|
} else {
|
|
r.label = ETICHETA.FARA_DOVADA;
|
|
r.evidence.push(TIP_HASH[t.type]);
|
|
// a spend seen never reaches here (it is "exposed" or "unmeasured" above)
|
|
r.evidence.push(`no spend of this address in the ${obs.txCount ?? ins.length + outs.length} confirmed transaction(s) served by the target`);
|
|
}
|
|
// what the data source says about the outputs: Bitcoin Core from its unspent set, an index from its statistics, Electrum from the pages
|
|
if (Number.isFinite(obs.unspentFound)) r.evidence.push(`${obs.spentAbsent} of ${outs.length} output(s) absent from the node's unspent set (scantxoutset), ${spends.length} spend(s) read from the given transaction ids`);
|
|
else if (obs.spentOutputs !== undefined && obs.spentOutputs !== null) r.evidence.push(`${obs.spentOutputs} of ${outs.length} output(s) spent, as served` + (Number.isFinite(obs.spentOutputsServed) ? ` (the target counts ${obs.spentOutputsServed} spent output(s))` : ''));
|
|
}
|
|
if (obs.unconfirmedCount) r.notSeen.push(`${obs.unconfirmedCount} unconfirmed transaction(s) ${neconf} were not used as on-chain evidence`);
|
|
if (obs.note) r.notSeen.push(obs.note);
|
|
r.notSeen.push(OFF_CHAIN);
|
|
return r;
|
|
}
|
|
|
|
/** mode 1, Ethereum */
|
|
export function clasificaEthOffline(a) {
|
|
const r = { input: a.input, chain: 'ethereum', address: a.address, checksum: a.checksum, mode: 1, evidence: [], notSeen: [], label: null, unmeasured: null };
|
|
if (a.error) return nem(r, 'not classified: ' + a.error);
|
|
r.label = ETICHETA.FARA_DOVADA;
|
|
r.evidence.push('by address form: an Ethereum address is keccak256 of the public key; the key is not on chain until the account signs a transaction or an EIP-7702 authorization');
|
|
r.notSeen.push('transactions sent from this address (mode 1 reads no chain data; the first one reveals the key from its signature)');
|
|
r.notSeen.push('whether the address is a contract account (no key of its own), an externally owned account, or an account with an EIP-7702 delegation');
|
|
r.notSeen.push(OFF_CHAIN_ETH);
|
|
r.notSeen.push(ALTE_LANTURI_ETH(null));
|
|
return r;
|
|
}
|
|
|
|
/**
|
|
* mode 2/3, Ethereum: from observations (adaptoare.mjs, ethRpc), then the HEAD the endpoint served
|
|
* obs = { chainId, blockNumber, head: { number, time }, stateRoot, accountAbsent, syncing: null | { currentBlock, highestBlock }, code, nonce (both
|
|
* proven under stateRoot, the state root of the head block: adaptoare.mjs ethRpc), window: [a, b], scanned,
|
|
* sentTx: { hash, block, type, recovered }, authorization: { hash, block, index, delegate, txHashMatches }, partial, error }
|
|
* ctx = { referinta (ms), referintaText }, as for Bitcoin
|
|
* Order: a head more than MARJA_VARF_ORE hours after the reference instant gives no label (C); the account itself (clasificaEthUna); every
|
|
* verdict names the head; "no on-chain evidence" only from an endpoint that does not report that it is syncing and whose head time is at
|
|
* most MARJA_CAP_ETH_MIN minutes before the reference instant (B), and never without a head read
|
|
*/
|
|
export function clasificaEthObservat(a, obs, mode, ctx = {}) {
|
|
let r = clasificaEthUna(a, obs, mode);
|
|
if (a.error || !obs || obs.error || obs.partial) return r;
|
|
if (!obs.head) return r.label === ETICHETA.FARA_DOVADA ? nem(r, 'not measured: the head block of the endpoint was not read, so what it served cannot be placed against the chain') : r;
|
|
const linieCap = `head block ${obs.head.number} served by the endpoint (block time ${isoS(obs.head.time)})${obs.syncing ? ', which reports that it is syncing' : ''}`;
|
|
const dupa = dupaDataRaportului(obs.head.time, `its head block ${obs.head.number} carries the time`, ctx, 'the endpoint');
|
|
if (dupa) return { ...r, label: null, unmeasured: dupa, evidence: [linieCap], notSeen: [] };
|
|
r.evidence.unshift(linieCap);
|
|
if (r.label === ETICHETA.FARA_DOVADA) {
|
|
const deCe = 'a transaction sent or an authorization signed after its head is not in what it served; re-run against an endpoint that has caught up with the chain';
|
|
if (obs.syncing) r = nem(r, `not measured: endpoint behind the chain: it reports that it is syncing (eth_syncing: current block ${obs.syncing.currentBlock ?? 'not given'}, highest block ${obs.syncing.highestBlock ?? 'not given'}; head block ${obs.head.number} at ${isoS(obs.head.time)}); ${deCe}`);
|
|
else if (!Number.isFinite(ctx.referinta)) r = nem(r, `not measured: head block ${obs.head.number} at ${isoS(obs.head.time)} was read, but no reference instant was given to judge it against`);
|
|
else if (obs.head.time * 1000 < ctx.referinta - MARJA_CAP_ETH_MIN * 60 * 1000) r = nem(r, `not measured: endpoint behind the chain: head block ${obs.head.number} at ${isoS(obs.head.time)}, more than ${MARJA_CAP_ETH_MIN} minutes before ${ctx.referintaText || 'the reference instant'}; ${deCe}`);
|
|
}
|
|
return r;
|
|
}
|
|
/** one Ethereum account, from its own observation (the rules of the header comment) */
|
|
function clasificaEthUna(a, obs, mode) {
|
|
const r = { input: a.input, chain: 'ethereum', address: a.address, checksum: a.checksum, mode, evidence: [], notSeen: [], label: null, unmeasured: null };
|
|
if (a.error) return nem(r, 'not classified: ' + a.error);
|
|
if (!obs || obs.error) return nem(r, 'not measured: ' + (obs && obs.error ? obs.error : 'no observation'));
|
|
if (obs.partial) return nem(r, 'not measured: ' + obs.partial);
|
|
const chain = obs.chainId !== undefined ? ` (chain id ${obs.chainId})` : '';
|
|
const nonce = obs.nonce === undefined || obs.nonce === null ? null : Number(BigInt(obs.nonce));
|
|
const [w0, w1] = obs.window || [null, null];
|
|
const fereastra = obs.window ? `blocks ${w0}..${w1} (${obs.scanned} block(s) read)` : 'no block read';
|
|
const cheieDinTx = () => { const s = obs.sentTx, rec = (s && s.recovered) || {}; return s && rec.ok && rec.address === a.address && rec.txHashMatches ? `public key recovered from the signature of transaction ${s.hash} in block ${s.block}${chain} (type 0x${Number(s.type || 0).toString(16)}); keccak256 of that key gives this address; the transaction hash re-derived from the served fields matches` : null; };
|
|
// an authorization counts only when the transaction that carries it hashes to the hash the endpoint gives (re-derived from its fields)
|
|
const autorizare = () => (obs.authorization && obs.authorization.txHashMatches ? `authorization signed by this account in transaction ${obs.authorization.hash} (block ${obs.authorization.block}, authorization ${obs.authorization.index}, delegate ${obs.authorization.delegate}): the authority recovered from the authorization's signature is this address; the transaction hash re-derived from the served fields matches` : null);
|
|
const autorizareNepotrivita = () => (obs.authorization && !obs.authorization.txHashMatches ? `the endpoint serves an authorization signed by this account in transaction ${obs.authorization.hash} (block ${obs.authorization.block}), but the served fields of that transaction do not hash to that hash; not used as evidence` : null);
|
|
|
|
// EIP-7702: the delegation designator is in the account's code only after an authorization signed with the account's key was included
|
|
if (esteDelegare7702(obs.code)) {
|
|
r.label = ETICHETA.EXPUSA;
|
|
r.evidence.push(`EIP-7702 delegation designator in the account code at head block ${obs.blockNumber}${chain} (its code hash proven under the state root of that block): 0xef0100 followed by the delegate ${delegatDin7702(obs.code)}; the chain sets this code only after an authorization signed with this account's own key was included, so the key has signed on chain`);
|
|
const au = autorizare(), auRea = autorizareNepotrivita(); if (au) r.evidence.push(au); else if (auRea) r.notSeen.push(auRea); else r.notSeen.push(`the authorizing transaction itself: none found in ${fereastra}; widen --eth-blocks to locate it`);
|
|
const tx = cheieDinTx(); if (tx) r.evidence.push(tx);
|
|
if (nonce !== null) r.evidence.push(`nonce ${nonce} at head block ${obs.blockNumber}, proven under its state root (eth_getProof)`);
|
|
return r;
|
|
}
|
|
const codeBytes = obs.code && obs.code !== '0x' ? (obs.code.length - 2) / 2 : 0;
|
|
if (codeBytes > 0) {
|
|
r.label = ETICHETA.FARA_DOVADA;
|
|
r.evidence.push(`contract account${chain}: ${codeBytes} bytes of code at head block ${obs.blockNumber} (its hash proven under the state root of that block); no secp256k1 key is bound to the address itself`);
|
|
r.notSeen.push('the keys that control this contract (owners, signers, upgrade keys): not visible from the address, not classified by this tool');
|
|
r.notSeen.push(OFF_CHAIN_ETH);
|
|
r.notSeen.push(ALTE_LANTURI_ETH(obs.chainId));
|
|
return r;
|
|
}
|
|
// a window in which no block was read measured nothing: a label on the nonce alone would rest on what this tool calls "not evidence"
|
|
if (obs.window && !(obs.scanned > 0)) return nem(r, `not measured: no block was read in ${fereastra}; the window must cover at least one block at or below the head ${obs.blockNumber}`);
|
|
if (obs.sentTx) {
|
|
const tx = cheieDinTx();
|
|
if (tx) { r.label = ETICHETA.EXPUSA; r.evidence.push(tx); if (nonce !== null) r.evidence.push(`nonce ${nonce} at head block ${obs.blockNumber}, proven under its state root (eth_getProof)`); return r; }
|
|
const s = obs.sentTx, rec = s.recovered || {};
|
|
return nem(r, `not measured: the endpoint serves transaction ${s.hash} in block ${s.block} as sent from this address, but the key could not be re-derived here (${rec.ok ? (rec.txHashMatches ? 'the recovered key gives ' + rec.address : 'the served fields do not hash to the transaction hash') : rec.reason})`);
|
|
}
|
|
if (autorizareNepotrivita()) return nem(r, 'not measured: ' + autorizareNepotrivita());
|
|
if (obs.authorization) {
|
|
r.label = ETICHETA.EXPUSA;
|
|
r.evidence.push(autorizare() + '; the account code at the head block is empty (the delegation was cleared or set to the zero address), the key has signed on chain');
|
|
if (nonce !== null) r.evidence.push(`nonce ${nonce} at head block ${obs.blockNumber}, proven under its state root (eth_getProof)`);
|
|
return r;
|
|
}
|
|
if (nonce === null) return nem(r, 'not measured: no nonce was proven for this account');
|
|
if (nonce > 0) return nem(r, `not measured: the account proof at head block ${obs.blockNumber} gives nonce ${nonce} (transactions sent), but no transaction or authorization of this address was found in ${fereastra}; widen --eth-blocks to the block of a known transaction`);
|
|
r.label = ETICHETA.FARA_DOVADA;
|
|
r.evidence.push(`externally owned account${chain}: no code and nonce 0 at head block ${obs.blockNumber}, both proven under its state root (eth_getProof: every node of the proof checked by its hash from that root); no transaction sent and no EIP-7702 authorization signed by this address in ${fereastra}`);
|
|
r.notSeen.push(`transactions and authorizations in blocks outside ${w0}..${w1}`);
|
|
r.notSeen.push('the head block itself: its number, its time and its state root are what the endpoint served, and this tool does not check that block against the consensus of the chain (its hash, its parent, the validators that attested it): the account proof binds the nonce and the code to the state root of that block, not that block to the canonical chain');
|
|
r.notSeen.push(OFF_CHAIN_ETH);
|
|
r.notSeen.push(ALTE_LANTURI_ETH(obs.chainId));
|
|
return r;
|
|
}
|
|
|
|
/** the summary line and counts of a list of classified addresses */
|
|
export function rezumat(lista, mode) {
|
|
const n = lista.length;
|
|
const exp = lista.filter((x) => x.label === ETICHETA.EXPUSA).length;
|
|
const fara = lista.filter((x) => x.label === ETICHETA.FARA_DOVADA).length;
|
|
const nemasurate = lista.filter((x) => x.unmeasured).length;
|
|
return { addresses: n, exposed: exp, noOnChainEvidence: fara, unmeasured: nemasurate, mode,
|
|
verdict: `${n} address(es), mode ${mode}: ${exp} ${ETICHETA.EXPUSA}, ${fara} ${ETICHETA.FARA_DOVADA}, ${nemasurate} ${NEMASURAT}` };
|
|
}
|