merge changes

This commit is contained in:
Alex Bosworth 2022-09-05 13:13:36 -07:00
parent d7eecf3de4
commit 6959baccf7
No known key found for this signature in database
GPG key ID: E80D2F3F311FD87E
8 changed files with 289 additions and 110 deletions

14
bos
View file

@ -835,28 +835,28 @@ prog
.command('inbound-channel-rules', 'Enforce rules for inbound channels')
.help('Rules should be written evaluating to TRUE to accept a channel')
.help('Example rule: --rule "CAPACITY > 100000"')
.help('Avoid requiring Blockchain confirmations using --trust-funding-from')
.help('For formulas: CAPACITIES are the sizes of the peer public channels')
.help('For formulas: CAPACITY is the size of the requested channel open')
.help('For formulas: CHANNEL_AGES are the block ages of public channels')
.help('For formulas: FEE_RATES are the outbound fee rates for the peer')
.help('For formulas: IS_TRUSTED_FUNDING is allowing acceptance of trusted funding channel')
.help('For formulas: LOCAL_BALANCE is the gifted amount from the peer')
.help('For formulas: PUBLIC_KEY is the public key of the requesting peer')
.option('--coop-close-address', 'Request using a cooperative close address')
.option('--node <node_name>', 'Saved node to reject inbound channels on')
.option('--reason <message>', 'Message to return when rejecting a request')
.option('--rule <formula>', 'Freeform rule for inbound channel', REPEATABLE)
.option('--trust-funding-from <trust_funding_from_pubkey>', 'Allow trusted funding from pubkey', REPEATABLE)
.option('--trust-funding-from <key>', 'Trust channel funds from', REPEATABLE)
.action((args, options, logger) => {
return new Promise(async (resolve, reject) => {
try {
return await peers.interceptInboundChannels({
logger,
address: options.coopCloseAddress || undefined,
keys: collect(options.trustFundingFrom),
lnd: (await lndForNode(logger, options.node)).lnd,
reason: options.reason,
rules: flatten([options.rule].filter(n => !!n)),
trust: collect(options.trustFundingFrom),
});
} catch (err) {
return logger.error({err}) && reject();
@ -1125,9 +1125,11 @@ prog
.command('open', 'Open channels, optionally using an external wallet')
.help('When creating channels from an external wallet do not self-broadcast')
.help('Skip external option by specifying --internal-fund-at-fee-rate')
.help('For trusted funding specify --type as private-trusted/public-trusted')
.help('Trusted channel funding is not supported in LND 0.15.0 and below')
.argument('<peer_public_keys...>', 'With nodes with public keys')
.option('--amount <channel_capacity>', 'Capacities to open', REPEATABLE)
.option('--avoid-broadcast <avoid_trusted_funding_broadcast>', 'Avoid broadcasting trusted funding channels')
.option('--avoid-broadcast', 'Avoid broadcasting channel open transaction')
.option('--coop-close-address <addr>', 'Coop-close address', REPEATABLE)
.option('--external-funding', 'Use external funds for the channel open')
.option('--give <give_amount>', 'Amount to gift to peer', REPEATABLE)
@ -1135,19 +1137,19 @@ prog
.option('--node <node_name>', 'Saved node to open channels')
.option('--opening-node <node_name>', 'Open with saved node', REPEATABLE)
.option('--set-fee-rate <ppm>', 'Set forward fee rate to peer', REPEATABLE)
.option('--type <type>', 'Type of channel (private/private-trusted/public/public-trusted)', REPEATABLE)
.option('--type <type>', 'Type of channel (private/public)', REPEATABLE)
.action((args, options, logger) => {
return new Promise(async (resolve, reject) => {
try {
return peers.openChannels({
logger,
ask: await commands.interrogate({}),
avoid_broadcast: options.avoidBroadcast || undefined,
capacities: collect(options.amount),
cooperative_close_addresses: collect(options.coopCloseAddress),
fs: {getFile: readFile},
gives: collect(options.give),
internal_fund_fee_rate: options.internalFundAtFeeRate || undefined,
is_avoiding_broadcast: options.avoidBroadcast || undefined,
is_external: options.externalFunding,
lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
opening_nodes: collect(options.openingNode),

View file

@ -1,7 +1,8 @@
const defaultChannelCapacity = 5e6;
const uniq = arr => Array.from(new Set(arr));
const isTrusted = type => ['private-trusted', 'public-trusted'].includes(type);
const privateTypes = ['private', 'private-trusted'];
const trustedFundingTypes = ['private-trusted', 'public-trusted'];
const uniq = arr => Array.from(new Set(arr));
/** Derive channel to open details from channel argument list
@ -37,7 +38,7 @@ module.exports = args => {
cooperative_close_address: args.addresses[i] || undefined,
give_tokens: !!args.gives[i] ? Number(args.gives[i]) : undefined,
is_private: !!args.types[i] && privateTypes.includes(args.types[i]),
is_trusted_funding: !!args.types[i] && trustedFundingTypes.includes(args.types[i]),
is_trusted_funding: !!args.types[i] && isTrusted(args.types[i]),
node: args.saved[i] || undefined,
partner_public_key: key,
rate: args.rates[i] || undefined,

View file

@ -55,6 +55,11 @@ module.exports = (args, cbk) => {
// Get the capacities fees for the node to use in rule parsing
getNodeFees: ['validate', ({}, cbk) => {
// Exit early when there are no rules to evaluate
if (!args.rules.length) {
return cbk(null, {});
}
return getNode({
lnd: args.lnd,
public_key: args.partner_public_key,

View file

@ -0,0 +1,68 @@
const asyncAuto = require('async/auto');
const {getChannels} = require('ln-service');
const {getPendingChannels} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const chanAsOutpoint = n => `${n.transaction_id}:${n.transaction_vout}`;
const uniq = arr => Array.from(new Set(arr));
/** Get channel outpoints to check for open channel publish safety
{
lnd: <Authenticated LND API Object>
}
@returns via cbk or Promise
{
channels: [{
transaction_id: <Transaction Id Hex String>
transaction_vout: <Transaction Output Index Number>
}]
}
*/
module.exports = ({lnd}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!lnd) {
return cbk([400, 'ExpectedAuthenticatedLndToGetChannelOutpoints']);
}
return cbk();
},
// Get regular channel outpoints
getChannels: ['validate', ({}, cbk) => getChannels({lnd}, cbk)],
// Get pending channel outpoints
getPending: ['validate', ({}, cbk) => getPendingChannels({lnd}, cbk)],
// Assemble channel outpoints
outpoints: [
'getChannels',
'getPending',
({getChannels, getPending}, cbk) =>
{
const channelOutpoints = getChannels.channels.map(channel => ({
transaction_id: channel.transaction_id,
transaction_vout: channel.transaction_vout,
}));
const pendingOutpoints = getPending.pending_channels.map(channel => ({
transaction_id: channel.transaction_id,
transaction_vout: channel.transaction_vout,
}));
const channels = [].concat(channelOutpoints).concat(pendingOutpoints);
if (uniq(channels.map(chanAsOutpoint)).length !== channels.length) {
return cbk([503, 'UnexpectedDuplicateOutpointWithPendingChannels']);
}
return cbk(null, {channels});
}],
},
returnResult({reject, resolve, of: 'outpoints'}, cbk));
});
};

View file

@ -21,9 +21,10 @@ const {toOutputScript} = address;
logger: <Winston Logger Object>
[reason]: <Reason Error Message String>
rules: [<Rule for Inbound Channel String>]
trust: [<Trust Funding From Node With Identity Public Key Hex String>]
}
*/
module.exports = ({address, keys, lnd, logger, reason, rules}, cbk) => {
module.exports = ({address, lnd, logger, reason, rules, trust}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
@ -44,12 +45,8 @@ module.exports = ({address, keys, lnd, logger, reason, rules}, cbk) => {
return cbk([400, 'ExpectedArrayOfRejectRulesToRejectChannels']);
}
if (!isArray(keys)) {
return cbk([400, 'ExpectedArrayOfPublicKeysToInterceptChannels']);
}
if (!!keys.filter(n => !isPublicKey(n)).length) {
return cbk([400, 'ExpectedValidPublicKeysToInterceptChannels']);
if (!!trust.filter(n => !isPublicKey(n)).length) {
return cbk([400, 'ExpectedValidTrustPublicKeysToInterceptChannels']);
}
if (!!rules.length) {
@ -69,6 +66,10 @@ module.exports = ({address, keys, lnd, logger, reason, rules}, cbk) => {
}
}
if (!isArray(trust)) {
return cbk([400, 'ExpectedArrayOfTrustedKeysToInterceptChannels']);
}
return cbk();
},
@ -114,16 +115,28 @@ module.exports = ({address, keys, lnd, logger, reason, rules}, cbk) => {
logger.info({
enforcing_inbound_channel_rules: rules,
requesting_cooperative_close_address: address,
trusted_funding_pubkeys: !!keys.length ? keys.filter(n => !!n) : undefined,
do_not_require_conf_funds_from: !!trust.length ? trust : undefined,
});
sub.on('channel_request', request => {
const peerId = request.partner_public_key;
// Exit early when requester is not trusted for trusted funding
if (!!request.is_trusted_funding && !trust.includes(peerId)) {
logger.info({
rejected: peerId,
reason: {trusted_funding_not_configured_for_peer: true},
});
return request.reject({reason: 'TrustedFundingAccessDenied'});
}
return detectOpenRuleViolation({
lnd,
rules,
capacity: request.capacity,
local_balance: request.local_balance,
partner_public_key: request.partner_public_key,
partner_public_key: peerId,
},
(err, res) => {
if (!!err) {
@ -136,7 +149,7 @@ module.exports = ({address, keys, lnd, logger, reason, rules}, cbk) => {
// Exit early when a channel open rule violation rejects a channel
if (!!res.rule) {
logger.info({
rejected: request.partner_public_key,
rejected: peerId,
capacity: request.capacity,
rule: res.rule,
});
@ -144,32 +157,11 @@ module.exports = ({address, keys, lnd, logger, reason, rules}, cbk) => {
return request.reject({reason});
}
// Exit early if no allow list is specified and trusted funding is true
if (!keys.length && !!request.is_trusted_funding) {
logger.info({
rejected: request.partner_public_key,
reason: {
is_trusted_funding: true
}
});
return request.reject({reason: 'PublicKeyNotWhitelistedForTrustedFunding'});
}
// Exit early if requester pubkey is not allowed for trusted funding
if (!!keys.length && !!request.is_trusted_funding && !keys.includes(request.partner_public_key)) {
logger.info({
rejected: request.partner_public_key,
reason: {
is_trusted_funding: true
}
});
return request.reject({reason: 'PublicKeyNotWhitelistedForTrustedFunding'});
}
// Accept the channel open request
return request.accept({cooperative_close_address: address, is_trusted_funding: request.is_trusted_funding});
return request.accept({
cooperative_close_address: address,
is_trusted_funding: request.is_trusted_funding,
});
});
return;

View file

@ -15,12 +15,11 @@ const asyncRetry = require('async/retry');
const {broadcastChainTransaction} = require('ln-service');
const {cancelPendingChannel} = require('ln-service');
const {fundPendingChannels} = require('ln-service');
const {getFundedTransaction} = require('ln-sync');
const {getChannels} = require('ln-service');
const {getFundedTransaction} = require('ln-sync');
const {getNetwork} = require('ln-sync');
const {getNode} = require('ln-service');
const {getPeers} = require('ln-service');
const {getPendingChannels} = require('ln-service');
const {getPsbtFromTransaction} = require('goldengate');
const {getWalletVersion} = require('ln-service');
const {openChannels} = require('ln-service');
@ -34,6 +33,7 @@ const adjustFees = require('./../routing/adjust_fees');
const {authenticatedLnd} = require('./../lnd');
const channelsFromArguments = require('./channels_from_arguments');
const {getAddressUtxo} = require('./../chain');
const getChannelOutpoints = require('./get_channel_outpoints');
const {parseAmount} = require('./../display');
const bech32AsData = bech32 => address.fromBech32(bech32).data;
@ -54,7 +54,6 @@ const per = (a, b) => (a / b).toFixed(2);
const relockIntervalMs = 1000 * 20;
const times = 10;
const tokAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
const trustedFundingTypes = ['private-trusted', 'public-trusted'];
const uniq = arr => Array.from(new Set(arr));
const utxoPollingIntervalMs = 1000 * 30;
const utxoPollingTimes = 20;
@ -63,13 +62,13 @@ const utxoPollingTimes = 20;
{
ask: <Ask For Input Function>
[avoid_broadcast]: <Avoid Broadcast Bool>
capacities: [<New Channel Capacity Tokens String>]
cooperative_close_addresses: [<Cooperative Close Address>]
fs: {
getFile: <Read File Contents Function> (path, cbk) => {}
}
gives: [<New Channel Give Tokens Number>]
[is_avoiding_broadcast]: <Avoid Funding Transaction Broadcast Bool>
[is_external]: <Use External Funds to Open Channels Bool>
lnd: <Authenticated LND API Object>
logger: <Winston Logger Object>
@ -166,7 +165,7 @@ module.exports = (args, cbk) => {
}
if (args.types.findIndex(n => !knownTypes.includes(n)) !== notFound) {
return cbk([400, 'UnknownChannelType']);
return cbk([400, 'UnknownChannelType', {channel_types: knownTypes}]);
}
if (!!args.types.length && args.types.length !== publicKeysLength) {
@ -189,11 +188,6 @@ module.exports = (args, cbk) => {
return cbk(null, capacities);
}],
// Get the wallet version to make sure the node supports internal funding
getWalletVersion: ['validate', ({}, cbk) => {
return getWalletVersion({lnd: args.lnd}, cbk);
}],
// Get LNDs associated with nodes specified for opening
getLnds: ['validate', ({}, cbk) => {
// Exit early when there are no opening nodes specified
@ -244,6 +238,11 @@ module.exports = (args, cbk) => {
cbk);
}],
// Get the wallet version to make sure the node supports internal funding
getWalletVersion: ['validate', ({}, cbk) => {
return getWalletVersion({lnd: args.lnd}, cbk);
}],
// Get the networks of the opening nodes
getOpeningNetworks: ['getLnds', ({getLnds}, cbk) => {
if (!getLnds) {
@ -720,51 +719,13 @@ module.exports = (args, cbk) => {
cbk);
})],
// Get list of trusted channels
getTrustedChannels: [
'fundChannels',
'fundingPsbt',
'getFunding',
'openChannels',
'outputs',
({getFunding, openChannels}, cbk) => {
// Exit early if no trusted channels are being opened
const trustedChannelsLength = (args.types.filter(n => (n === 'public-trusted' || n === 'private-trusted'))).length;
if (!trustedChannelsLength) {
return cbk(null, []);
}
return asyncRetry({interval, times: pendingCheckTimes}, cbk => {
return asyncMap(openChannels, ({lnd, node, pending}, cbk) => {
return getChannels({lnd}, cbk);
},
(err, res) => {
if (!!err) {
return cbk([400, 'FailedToGetTrustedChannels']);
}
const txId = fromHex(getFunding.value.transaction).getId();
const trustedChannels = res[0].channels.filter(n => !!n.is_trusted_funding && n.transaction_id === txId);
if (!trustedChannels.length || trustedChannels.length !== trustedChannelsLength) {
return cbk([400, 'FailedToFindTrustedChannelsList']);
}
return cbk(null, trustedChannels);
},
cbk)
},
cbk);
}],
// Broadcast the funding transaction when opening on multiple nodes
broadcastChainTransaction: [
'fundChannels',
'fundingPsbt',
'getFunding',
'getTrustedChannels',
'openChannels',
({fundChannels, fundingPsbt, getFunding, getTrustedChannels, openChannels}, cbk) =>
({fundChannels, fundingPsbt, getFunding, openChannels}, cbk) =>
{
const fundingError = getFunding.error || fundingPsbt.error;
const error = fundChannels.error || fundingError;
@ -773,7 +734,7 @@ module.exports = (args, cbk) => {
if (!!error || !!fundingError) {
return cbk();
}
const toOpen = flatten(openChannels.map(n => n.pending));
const txId = fromHex(getFunding.value.transaction).getId();
@ -782,34 +743,36 @@ module.exports = (args, cbk) => {
// Make sure that pending channels are showing up: got commitment tx
return asyncRetry({interval, times: pendingCheckTimes}, cbk => {
return asyncMap(openChannels, ({lnd, node, pending}, cbk) => {
return getPendingChannels({lnd}, cbk);
return getChannelOutpoints({lnd}, cbk);
},
(err, res) => {
if (!!err) {
return cbk(err);
}
// Consolidate all pending channels from all nodes
const pending = flatten(res.map(n => n.pending_channels));
// Only consider pending channels related to this funding tx
// Consolidate all channels from all nodes
const pending = flatten(res.map(n => n.channels));
// Only consider channels related to this funding tx
const opening = pending.filter(n => n.transaction_id === txId);
// Every channel to open should be reflected in a pending channel
if ((opening.length + getTrustedChannels.length) !== toOpen.length) {
// Every channel to open should be reflected in a channel
if (opening.length !== toOpen.length) {
return cbk([503, 'FailedToFindPendingChannelOpen']);
}
// Exit early if avoiding broadcast
if (!!args.avoid_broadcast) {
args.logger.info({
args.logger.info({
raw_transaction_to_broadcast: getFunding.value.transaction,
is_avoiding_broadcast: true,
});
// Exit early when avoiding broadcast
if (!!args.is_avoiding_broadcast) {
args.logger.info({is_avoiding_broadcast: true});
return cbk();
}
args.logger.info({broadcasting: getFunding.value.transaction});
args.logger.info({broadcasting: getFunding.value.id});
return broadcastChainTransaction({
lnd: args.lnd,
@ -947,11 +910,10 @@ module.exports = (args, cbk) => {
'setFeeRates',
({getFunding, fundingPsbt}, cbk) =>
{
if (!!args.avoid_broadcast) {
return cbk();
}
return cbk(null, {transaction_id: getFunding.value.id});
return cbk(null, {
transaction: getFunding.value.transaction,
transaction_id: getFunding.value.id,
});
}],
},
returnResult({reject, resolve, of: 'completed'}, cbk));

View file

@ -0,0 +1,145 @@
const {addPeer} = require('ln-service');
const asyncAuto = require('async/auto');
const asyncEach = require('async/each');
const asyncRetry = require('async/retry');
const {createChainAddress} = require('ln-service');
const {fundPsbt} = require('ln-service');
const {getChannels} = require('ln-service');
const {getPendingChannels} = require('ln-service');
const {getWalletInfo} = require('ln-service');
const {openChannel} = require('ln-service');
const {signPsbt} = require('ln-service');
const {spawnLightningCluster} = require('ln-docker-daemons');
const {test} = require('@alexbosworth/tap');
const {Transaction} = require('bitcoinjs-lib');
const {interceptInboundChannels} = require('./../../peers');
const {openChannels} = require('./../../peers');
const count = 100;
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const interval = 200;
const log = () => {};
const size = 2;
const times = 1000;
// Opening trusted channels should open channels with specified nodes
test(`Open channels`, async ({end, equal, strictSame}) => {
const {kill, nodes} = await spawnLightningCluster({
size,
lnd_configuration: [
'--maxpendingchannels=10',
'--protocol.option-scid-alias',
'--protocol.zero-conf',
],
});
const [{generate, id, lnd}, target] = nodes;
try {
await generate({count});
await asyncRetry({interval, times}, async () => {
await addPeer({lnd, public_key: target.id, socket: target.socket});
await asyncEach(nodes, async ({lnd}) => {
const chain = await getWalletInfo({lnd});
if (!chain.is_synced_to_chain || !chain.is_synced_to_graph) {
throw new Error('WaitingForSync');
}
});
});
const {address} = await asyncRetry({interval, times}, async () => {
return await createChainAddress({lnd});
});
// Propose a trusted channel and accept it
await asyncAuto({
// Intercept the trusted open to accept it
intercept: async () => {
try {
return await interceptInboundChannels({
lnd: target.lnd,
logger: {error: () => {}, info: () => {}},
rules: [],
trust: [id],
});
} catch (err) {
// Interception will terminate after the channel is open
return;
}
},
// Propose the trusted channel to target
propose: async () => {
return asyncRetry({interval, times}, async () => {
await addPeer({lnd, public_key: target.id, socket: target.socket});
await openChannels({
lnd,
ask: async (args, cbk) => {
if (args.name === 'internal') {
return cbk({internal: false});
}
if (args.name === 'fund') {
const address = args.message.split(' ')[9];
const amount = args.message.split(' ')[7];
// Provide funding
const {psbt} = await fundPsbt({
lnd,
outputs: [{address, tokens: amount * 1e8}],
});
const signed = await signPsbt({lnd, psbt});
return cbk({fund: signed.psbt});
}
throw new Error('UnrecognizedParameter');
},
capacities: [],
cooperative_close_addresses: [],
fs: {getFile: () => {}},
gives: [],
logger: {info: log, error: log},
opening_nodes: [],
public_keys: [target.id],
request: () => {},
set_fee_rates: [],
types: ['private-trusted'],
});
});
},
// Generate blocks until the channel confirms
generate: async () => {
return await asyncRetry({interval, times}, async () => {
await generate({});
const {channels} = await getChannels({lnd});
if (!channels.length) {
throw new Error('Expected Channels');
}
const [channel] = channels;
equal(channel.is_trusted_funding, true, 'Trusted channel opened');
});
},
// Stop the target node
finish: ['generate', 'propose', async ({}) => {
await target.kill({});
}],
});
} catch (err) {
equal(err, null, 'Expected no error');
}
await kill({});
});

View file

@ -29,6 +29,7 @@ const tests = [
cooperative_close_address: 'address',
give_tokens: 1,
is_private: true,
is_trusted_funding: false,
node: undefined,
partner_public_key: Buffer.alloc(33, 3).toString('hex'),
rate: undefined,
@ -51,6 +52,7 @@ const tests = [
cooperative_close_address: undefined,
give_tokens: undefined,
is_private: false,
is_trusted_funding: false,
node: undefined,
partner_public_key: Buffer.alloc(33, 3).toString('hex'),
rate: undefined,
@ -77,6 +79,7 @@ const tests = [
cooperative_close_address: 'coopCloseAddressNodeA',
give_tokens: 3,
is_private: true,
is_trusted_funding: false,
node: 'savedA',
partner_public_key: 'remoteNodeA',
rate: '1',
@ -89,6 +92,7 @@ const tests = [
cooperative_close_address: 'coopCloseAddressNodeB',
give_tokens: 4,
is_private: false,
is_trusted_funding: false,
node: 'savedB',
partner_public_key: 'remoteNodeB',
rate: '2',