detect channels opened from change outputs
This commit is contained in:
parent
0c96a11150
commit
8f0830f6d1
@ -277,7 +277,7 @@ class ChannelsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getChannelForensicsByTransactionId(transactionId: string): Promise<any> {
|
public async $getChannelForensicsByClosingId(transactionId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
@ -294,7 +294,29 @@ class ChannelsApi {
|
|||||||
return rows[0];
|
return rows[0];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$getChannelForensicsByTransactionId error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$getChannelForensicsByClosingId error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
// don't throw - this data isn't essential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getChannelForensicsByOpeningId(transactionId: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
channels.id, channels.node1_public_key, channels.node2_public_key,
|
||||||
|
channels.status, channels.transaction_id,
|
||||||
|
forensics.*
|
||||||
|
FROM channels
|
||||||
|
LEFT JOIN channels_forensics as forensics ON forensics.channel_id = channels.id
|
||||||
|
WHERE channels.transaction_id = ?
|
||||||
|
`;
|
||||||
|
const [rows]: any = await DB.query(query, [transactionId]);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
rows[0].outputs = JSON.parse(rows[0].outputs);
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getChannelForensicsByOpeningId error: ' + (e instanceof Error ? e.message : e));
|
||||||
// don't throw - this data isn't essential
|
// don't throw - this data isn't essential
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -306,7 +306,7 @@ class NetworkSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private findLightningScript(vin: IEsploraApi.Vin): number {
|
private findLightningScript(vin: IEsploraApi.Vin): number {
|
||||||
const topElement = vin.witness[vin.witness.length - 2];
|
const topElement = vin.witness ? vin.witness[vin.witness.length - 2] : '';
|
||||||
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) {
|
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) {
|
||||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
|
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
|
||||||
if (topElement === '01') {
|
if (topElement === '01') {
|
||||||
@ -325,7 +325,7 @@ class NetworkSyncService {
|
|||||||
) {
|
) {
|
||||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
|
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
|
||||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
|
// https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
|
||||||
if (topElement.length === 66) {
|
if (topElement?.length === 66) {
|
||||||
// top element is a public key
|
// top element is a public key
|
||||||
// 'Revoked Lightning HTLC'; Penalty force closed
|
// 'Revoked Lightning HTLC'; Penalty force closed
|
||||||
return 4;
|
return 4;
|
||||||
@ -365,28 +365,35 @@ class NetworkSyncService {
|
|||||||
for (const openChannel of channels) {
|
for (const openChannel of channels) {
|
||||||
const openTx = await bitcoinApi.$getRawTransaction(openChannel.transaction_id);
|
const openTx = await bitcoinApi.$getRawTransaction(openChannel.transaction_id);
|
||||||
for (const input of openTx.vin) {
|
for (const input of openTx.vin) {
|
||||||
const closeChannel = await channelsApi.$getChannelForensicsByTransactionId(input.txid);
|
const closeChannel = await channelsApi.$getChannelForensicsByClosingId(input.txid);
|
||||||
if (closeChannel) {
|
if (closeChannel) {
|
||||||
// this input directly spends a channel close output
|
// this input directly spends a channel close output
|
||||||
await this.$attributeChannelBalances(closeChannel, openChannel, input);
|
await this.$attributeChannelBalances(closeChannel, openChannel, input);
|
||||||
} else {
|
} else {
|
||||||
// check if this input spends any swept channel close outputs
|
const prevOpenChannel = await channelsApi.$getChannelForensicsByOpeningId(input.txid);
|
||||||
await this.$attributeSweptChannelCloses(openChannel, input);
|
if (prevOpenChannel) {
|
||||||
|
await this.$attributeChannelBalances(prevOpenChannel, openChannel, input, null, null, true);
|
||||||
|
} else {
|
||||||
|
// check if this input spends any swept channel close outputs
|
||||||
|
await this.$attributeSweptChannelCloses(openChannel, input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// calculate how much of the total input value is attributable to the channel open output
|
// calculate how much of the total input value is attributable to the channel open output
|
||||||
openChannel.funding_ratio = openTx.vout[openChannel.transaction_vout].value / ((openTx.vout.reduce((sum, v) => sum + v.value, 0) || 1) + openTx.fee);
|
openChannel.funding_ratio = openTx.vout[openChannel.transaction_vout].value / ((openTx.vout.reduce((sum, v) => sum + v.value, 0) || 1) + openTx.fee);
|
||||||
// save changes to the opening channel, and mark it as checked
|
// save changes to the opening channel, and mark it as checked
|
||||||
if (openTx.vin.length === 1) {
|
if (openTx?.vin?.length === 1) {
|
||||||
openChannel.single_funded = true;
|
openChannel.single_funded = true;
|
||||||
}
|
}
|
||||||
await channelsApi.$updateOpeningInfo(openChannel);
|
if (openChannel.node1_funding_balance || openChannel.node2_funding_balance || openChannel.node1_closing_balance || openChannel.node2_closing_balance || openChannel.closed_by) {
|
||||||
|
await channelsApi.$updateOpeningInfo(openChannel);
|
||||||
|
}
|
||||||
await channelsApi.$markChannelSourceChecked(openChannel.id);
|
await channelsApi.$markChannelSourceChecked(openChannel.id);
|
||||||
|
|
||||||
++progress;
|
++progress;
|
||||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
if (elapsedSeconds > 10) {
|
if (elapsedSeconds > 10) {
|
||||||
logger.info(`Updating channel opened channel forensics ${progress}/${channels.length}`);
|
logger.info(`Updating opened channel forensics ${progress}/${channels?.length}`);
|
||||||
this.loggerTimer = new Date().getTime() / 1000;
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -408,7 +415,7 @@ class NetworkSyncService {
|
|||||||
for (const sweepInput of sweepTx.vin) {
|
for (const sweepInput of sweepTx.vin) {
|
||||||
const lnScriptType = this.findLightningScript(sweepInput);
|
const lnScriptType = this.findLightningScript(sweepInput);
|
||||||
if (lnScriptType > 1) {
|
if (lnScriptType > 1) {
|
||||||
const closeChannel = await channelsApi.$getChannelForensicsByTransactionId(sweepInput.txid);
|
const closeChannel = await channelsApi.$getChannelForensicsByClosingId(sweepInput.txid);
|
||||||
if (closeChannel) {
|
if (closeChannel) {
|
||||||
const initiator = (lnScriptType === 2 || lnScriptType === 4) ? 'remote' : (lnScriptType === 3 ? 'local' : null);
|
const initiator = (lnScriptType === 2 || lnScriptType === 4) ? 'remote' : (lnScriptType === 3 ? 'local' : null);
|
||||||
await this.$attributeChannelBalances(closeChannel, openChannel, sweepInput, openContribution, initiator);
|
await this.$attributeChannelBalances(closeChannel, openChannel, sweepInput, openContribution, initiator);
|
||||||
@ -418,8 +425,8 @@ class NetworkSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async $attributeChannelBalances(
|
private async $attributeChannelBalances(
|
||||||
closeChannel, openChannel, input: IEsploraApi.Vin, openContribution: number | null = null,
|
prevChannel, openChannel, input: IEsploraApi.Vin, openContribution: number | null = null,
|
||||||
initiator: 'remote' | 'local' | null = null
|
initiator: 'remote' | 'local' | null = null, linkedOpenings: boolean = false
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// figure out which node controls the input/output
|
// figure out which node controls the input/output
|
||||||
let openSide;
|
let openSide;
|
||||||
@ -427,18 +434,18 @@ class NetworkSyncService {
|
|||||||
let closeRemote;
|
let closeRemote;
|
||||||
let matched = false;
|
let matched = false;
|
||||||
let ambiguous = false; // if counterparties are the same in both channels, we can't tell them apart
|
let ambiguous = false; // if counterparties are the same in both channels, we can't tell them apart
|
||||||
if (openChannel.node1_public_key === closeChannel.node1_public_key) {
|
if (openChannel.node1_public_key === prevChannel.node1_public_key) {
|
||||||
openSide = 1;
|
openSide = 1;
|
||||||
closeLocal = 1;
|
closeLocal = 1;
|
||||||
closeRemote = 2;
|
closeRemote = 2;
|
||||||
matched = true;
|
matched = true;
|
||||||
} else if (openChannel.node1_public_key === closeChannel.node2_public_key) {
|
} else if (openChannel.node1_public_key === prevChannel.node2_public_key) {
|
||||||
openSide = 1;
|
openSide = 1;
|
||||||
closeLocal = 2;
|
closeLocal = 2;
|
||||||
closeRemote = 1;
|
closeRemote = 1;
|
||||||
matched = true;
|
matched = true;
|
||||||
}
|
}
|
||||||
if (openChannel.node2_public_key === closeChannel.node1_public_key) {
|
if (openChannel.node2_public_key === prevChannel.node1_public_key) {
|
||||||
openSide = 2;
|
openSide = 2;
|
||||||
closeLocal = 1;
|
closeLocal = 1;
|
||||||
closeRemote = 2;
|
closeRemote = 2;
|
||||||
@ -446,7 +453,7 @@ class NetworkSyncService {
|
|||||||
ambiguous = true;
|
ambiguous = true;
|
||||||
}
|
}
|
||||||
matched = true;
|
matched = true;
|
||||||
} else if (openChannel.node2_public_key === closeChannel.node2_public_key) {
|
} else if (openChannel.node2_public_key === prevChannel.node2_public_key) {
|
||||||
openSide = 2;
|
openSide = 2;
|
||||||
closeLocal = 2;
|
closeLocal = 2;
|
||||||
closeRemote = 1;
|
closeRemote = 1;
|
||||||
@ -458,77 +465,78 @@ class NetworkSyncService {
|
|||||||
|
|
||||||
if (matched && !ambiguous) {
|
if (matched && !ambiguous) {
|
||||||
// fetch closing channel transaction and perform forensics on the outputs
|
// fetch closing channel transaction and perform forensics on the outputs
|
||||||
let closingTx: IEsploraApi.Transaction | undefined;
|
let prevChannelTx: IEsploraApi.Transaction | undefined;
|
||||||
let outspends: IEsploraApi.Outspend[] | undefined;
|
let outspends: IEsploraApi.Outspend[] | undefined;
|
||||||
try {
|
try {
|
||||||
closingTx = await bitcoinApi.$getRawTransaction(input.txid);
|
prevChannelTx = await bitcoinApi.$getRawTransaction(input.txid);
|
||||||
outspends = await bitcoinApi.$getOutspends(input.txid);
|
outspends = await bitcoinApi.$getOutspends(input.txid);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + closeChannel.closing_transaction_id + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
|
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + input.txid + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
|
||||||
}
|
}
|
||||||
if (!outspends || !closingTx) {
|
if (!outspends || !prevChannelTx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!closeChannel.outputs) {
|
if (!linkedOpenings) {
|
||||||
closeChannel.outputs = closeChannel.outputs || closingTx.vout.map(vout => {
|
if (!prevChannel.outputs) {
|
||||||
return {
|
prevChannel.outputs = prevChannel.outputs || prevChannelTx.vout.map(vout => {
|
||||||
type: 0,
|
return {
|
||||||
value: vout.value,
|
type: 0,
|
||||||
};
|
value: vout.value,
|
||||||
});
|
};
|
||||||
}
|
});
|
||||||
for (let i = 0; i < outspends.length; i++) {
|
|
||||||
const outspend = outspends[i];
|
|
||||||
const output = closeChannel.outputs[i];
|
|
||||||
if (outspend.spent && outspend.txid) {
|
|
||||||
try {
|
|
||||||
const spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid);
|
|
||||||
if (spendingTx) {
|
|
||||||
output.type = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
output.type = 0;
|
|
||||||
}
|
}
|
||||||
}
|
for (let i = 0; i < outspends?.length; i++) {
|
||||||
|
const outspend = outspends[i];
|
||||||
// attribute outputs to each counterparty, and sum up total known balances
|
const output = prevChannel.outputs[i];
|
||||||
closeChannel.outputs[input.vout].node = closeLocal;
|
if (outspend.spent && outspend.txid) {
|
||||||
const isPenalty = closeChannel.outputs.filter((out) => out.type === 2 || out.type === 4).length > 0;
|
try {
|
||||||
const normalOutput = [1,3].includes(closeChannel.outputs[input.vout].type);
|
const spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid);
|
||||||
let localClosingBalance = 0;
|
if (spendingTx) {
|
||||||
let remoteClosingBalance = 0;
|
output.type = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
||||||
for (const output of closeChannel.outputs) {
|
}
|
||||||
if (isPenalty) {
|
} catch (e) {
|
||||||
// penalty close, so local node takes everything
|
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
|
||||||
localClosingBalance += output.value;
|
}
|
||||||
} else if (output.node) {
|
|
||||||
// this output determinstically linked to one of the counterparties
|
|
||||||
if (output.node === closeLocal) {
|
|
||||||
localClosingBalance += output.value;
|
|
||||||
} else {
|
} else {
|
||||||
|
output.type = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// attribute outputs to each counterparty, and sum up total known balances
|
||||||
|
prevChannel.outputs[input.vout].node = closeLocal;
|
||||||
|
const isPenalty = prevChannel.outputs.filter((out) => out.type === 2 || out.type === 4)?.length > 0;
|
||||||
|
const normalOutput = [1,3].includes(prevChannel.outputs[input.vout].type);
|
||||||
|
let localClosingBalance = 0;
|
||||||
|
let remoteClosingBalance = 0;
|
||||||
|
for (const output of prevChannel.outputs) {
|
||||||
|
if (isPenalty) {
|
||||||
|
// penalty close, so local node takes everything
|
||||||
|
localClosingBalance += output.value;
|
||||||
|
} else if (output.node) {
|
||||||
|
// this output determinstically linked to one of the counterparties
|
||||||
|
if (output.node === closeLocal) {
|
||||||
|
localClosingBalance += output.value;
|
||||||
|
} else {
|
||||||
|
remoteClosingBalance += output.value;
|
||||||
|
}
|
||||||
|
} else if (normalOutput && (output.type === 1 || output.type === 3)) {
|
||||||
|
// local node had one main output, therefore remote node takes the other
|
||||||
remoteClosingBalance += output.value;
|
remoteClosingBalance += output.value;
|
||||||
}
|
}
|
||||||
} else if (normalOutput && (output.type === 1 || output.type === 3)) {
|
|
||||||
// local node had one main output, therefore remote node takes the other
|
|
||||||
remoteClosingBalance += output.value;
|
|
||||||
}
|
}
|
||||||
|
prevChannel[`node${closeLocal}_closing_balance`] = localClosingBalance;
|
||||||
|
prevChannel[`node${closeRemote}_closing_balance`] = remoteClosingBalance;
|
||||||
|
prevChannel.closing_fee = prevChannelTx.fee;
|
||||||
|
|
||||||
|
if (initiator && !linkedOpenings) {
|
||||||
|
const initiatorSide = initiator === 'remote' ? closeRemote : closeLocal;
|
||||||
|
prevChannel.closed_by = prevChannel[`node${initiatorSide}_public_key`];
|
||||||
|
}
|
||||||
|
|
||||||
|
// save changes to the closing channel
|
||||||
|
await channelsApi.$updateClosingInfo(prevChannel);
|
||||||
}
|
}
|
||||||
|
openChannel[`node${openSide}_funding_balance`] = openChannel[`node${openSide}_funding_balance`] + (openContribution || prevChannelTx?.vout[input.vout]?.value || 0);
|
||||||
openChannel[`node${openSide}_funding_balance`] = openChannel[`node${openSide}_funding_balance`] + (openContribution || closingTx?.vout[input.vout]?.value || 0);
|
|
||||||
closeChannel[`node${closeLocal}_closing_balance`] = localClosingBalance;
|
|
||||||
closeChannel[`node${closeRemote}_closing_balance`] = remoteClosingBalance;
|
|
||||||
closeChannel.closing_fee = closingTx.fee;
|
|
||||||
|
|
||||||
if (initiator) {
|
|
||||||
const initiatorSide = initiator === 'remote' ? closeRemote : closeLocal;
|
|
||||||
closeChannel.closed_by = closeChannel[`node${initiatorSide}_public_key`];
|
|
||||||
}
|
|
||||||
|
|
||||||
// save changes to the closing channel
|
|
||||||
await channelsApi.$updateClosingInfo(closeChannel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user