diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d7e32954c..42737c917 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -243,3 +243,16 @@ jobs: - name: run check run: make itest + + - name: Zip log files on failure + if: ${{ failure() }} + timeout-minutes: 5 + run: 7z a logs-itest.zip itest/**/*.log + + - name: Upload log files on failure + uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: logs-itest + path: logs-itest.zip + retention-days: 5 \ No newline at end of file diff --git a/Makefile b/Makefile index 876bc7816..380d66125 100644 --- a/Makefile +++ b/Makefile @@ -201,6 +201,10 @@ flake-unit: @$(call print, "Flake hunting unit tests.") while [ $$? -eq 0 ]; do GOTRACEBACK=all $(UNIT) -count=1; done +flake-itest-only: + @$(call print, "Flake hunting unit tests.") + while [ $$? -eq 0 ]; do make itest-only icase='${icase}'; done + # ========= # UTILITIES # ========= diff --git a/app/src/types/generated/lnd_pb.d.ts b/app/src/types/generated/lnd_pb.d.ts index a74a450e2..e959265e6 100644 --- a/app/src/types/generated/lnd_pb.d.ts +++ b/app/src/types/generated/lnd_pb.d.ts @@ -6131,6 +6131,30 @@ export namespace FeeReportResponse { } } +export class InboundFee extends jspb.Message { + getBaseFeeMsat(): number; + setBaseFeeMsat(value: number): void; + + getFeeRatePpm(): number; + setFeeRatePpm(value: number): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): InboundFee.AsObject; + static toObject(includeInstance: boolean, msg: InboundFee): InboundFee.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: InboundFee, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): InboundFee; + static deserializeBinaryFromReader(message: InboundFee, reader: jspb.BinaryReader): InboundFee; +} + +export namespace InboundFee { + export type AsObject = { + baseFeeMsat: number, + feeRatePpm: number, + } +} + export class PolicyUpdateRequest extends jspb.Message { hasGlobal(): boolean; clearGlobal(): void; @@ -6163,11 +6187,10 @@ export class PolicyUpdateRequest extends jspb.Message { getMinHtlcMsatSpecified(): boolean; setMinHtlcMsatSpecified(value: boolean): void; - getInboundBaseFeeMsat(): number; - setInboundBaseFeeMsat(value: number): void; - - getInboundFeeRatePpm(): number; - setInboundFeeRatePpm(value: number): void; + hasInboundFee(): boolean; + clearInboundFee(): void; + getInboundFee(): InboundFee | undefined; + setInboundFee(value?: InboundFee): void; getScopeCase(): PolicyUpdateRequest.ScopeCase; serializeBinary(): Uint8Array; @@ -6191,8 +6214,7 @@ export namespace PolicyUpdateRequest { maxHtlcMsat: string, minHtlcMsat: string, minHtlcMsatSpecified: boolean, - inboundBaseFeeMsat: number, - inboundFeeRatePpm: number, + inboundFee?: InboundFee.AsObject, } export enum ScopeCase { diff --git a/app/src/types/generated/lnd_pb.js b/app/src/types/generated/lnd_pb.js index f35942a83..0af195714 100644 --- a/app/src/types/generated/lnd_pb.js +++ b/app/src/types/generated/lnd_pb.js @@ -118,6 +118,7 @@ goog.exportSymbol('proto.lnrpc.HTLCAttempt', null, global); goog.exportSymbol('proto.lnrpc.HTLCAttempt.HTLCStatus', null, global); goog.exportSymbol('proto.lnrpc.Hop', null, global); goog.exportSymbol('proto.lnrpc.HopHint', null, global); +goog.exportSymbol('proto.lnrpc.InboundFee', null, global); goog.exportSymbol('proto.lnrpc.Initiator', null, global); goog.exportSymbol('proto.lnrpc.InstantUpdate', null, global); goog.exportSymbol('proto.lnrpc.InterceptFeedback', null, global); @@ -43966,6 +43967,175 @@ proto.lnrpc.FeeReportResponse.prototype.setMonthFeeSum = function(value) { +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.lnrpc.InboundFee = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.lnrpc.InboundFee, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.lnrpc.InboundFee.displayName = 'proto.lnrpc.InboundFee'; +} + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto suitable for use in Soy templates. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. + * @param {boolean=} opt_includeInstance Whether to include the JSPB instance + * for transitional soy proto support: http://goto/soy-param-migration + * @return {!Object} + */ +proto.lnrpc.InboundFee.prototype.toObject = function(opt_includeInstance) { + return proto.lnrpc.InboundFee.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Whether to include the JSPB + * instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.lnrpc.InboundFee} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.lnrpc.InboundFee.toObject = function(includeInstance, msg) { + var f, obj = { + baseFeeMsat: jspb.Message.getFieldWithDefault(msg, 1, 0), + feeRatePpm: jspb.Message.getFieldWithDefault(msg, 2, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.lnrpc.InboundFee} + */ +proto.lnrpc.InboundFee.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.lnrpc.InboundFee; + return proto.lnrpc.InboundFee.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.lnrpc.InboundFee} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.lnrpc.InboundFee} + */ +proto.lnrpc.InboundFee.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readInt32()); + msg.setBaseFeeMsat(value); + break; + case 2: + var value = /** @type {number} */ (reader.readInt32()); + msg.setFeeRatePpm(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.lnrpc.InboundFee.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.lnrpc.InboundFee.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.lnrpc.InboundFee} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.lnrpc.InboundFee.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getBaseFeeMsat(); + if (f !== 0) { + writer.writeInt32( + 1, + f + ); + } + f = message.getFeeRatePpm(); + if (f !== 0) { + writer.writeInt32( + 2, + f + ); + } +}; + + +/** + * optional int32 base_fee_msat = 1; + * @return {number} + */ +proto.lnrpc.InboundFee.prototype.getBaseFeeMsat = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** @param {number} value */ +proto.lnrpc.InboundFee.prototype.setBaseFeeMsat = function(value) { + jspb.Message.setProto3IntField(this, 1, value); +}; + + +/** + * optional int32 fee_rate_ppm = 2; + * @return {number} + */ +proto.lnrpc.InboundFee.prototype.getFeeRatePpm = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** @param {number} value */ +proto.lnrpc.InboundFee.prototype.setFeeRatePpm = function(value) { + jspb.Message.setProto3IntField(this, 2, value); +}; + + + /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -44047,8 +44217,7 @@ proto.lnrpc.PolicyUpdateRequest.toObject = function(includeInstance, msg) { maxHtlcMsat: jspb.Message.getFieldWithDefault(msg, 6, "0"), minHtlcMsat: jspb.Message.getFieldWithDefault(msg, 7, "0"), minHtlcMsatSpecified: jspb.Message.getFieldWithDefault(msg, 8, false), - inboundBaseFeeMsat: jspb.Message.getFieldWithDefault(msg, 10, 0), - inboundFeeRatePpm: jspb.Message.getFieldWithDefault(msg, 11, 0) + inboundFee: (f = msg.getInboundFee()) && proto.lnrpc.InboundFee.toObject(includeInstance, f) }; if (includeInstance) { @@ -44123,12 +44292,9 @@ proto.lnrpc.PolicyUpdateRequest.deserializeBinaryFromReader = function(msg, read msg.setMinHtlcMsatSpecified(value); break; case 10: - var value = /** @type {number} */ (reader.readInt32()); - msg.setInboundBaseFeeMsat(value); - break; - case 11: - var value = /** @type {number} */ (reader.readInt32()); - msg.setInboundFeeRatePpm(value); + var value = new proto.lnrpc.InboundFee; + reader.readMessage(value,proto.lnrpc.InboundFee.deserializeBinaryFromReader); + msg.setInboundFee(value); break; default: reader.skipField(); @@ -44223,18 +44389,12 @@ proto.lnrpc.PolicyUpdateRequest.serializeBinaryToWriter = function(message, writ f ); } - f = message.getInboundBaseFeeMsat(); - if (f !== 0) { - writer.writeInt32( + f = message.getInboundFee(); + if (f != null) { + writer.writeMessage( 10, - f - ); - } - f = message.getInboundFeeRatePpm(); - if (f !== 0) { - writer.writeInt32( - 11, - f + f, + proto.lnrpc.InboundFee.serializeBinaryToWriter ); } }; @@ -44409,32 +44569,32 @@ proto.lnrpc.PolicyUpdateRequest.prototype.setMinHtlcMsatSpecified = function(val /** - * optional int32 inbound_base_fee_msat = 10; - * @return {number} + * optional InboundFee inbound_fee = 10; + * @return {?proto.lnrpc.InboundFee} */ -proto.lnrpc.PolicyUpdateRequest.prototype.getInboundBaseFeeMsat = function() { - return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 10, 0)); +proto.lnrpc.PolicyUpdateRequest.prototype.getInboundFee = function() { + return /** @type{?proto.lnrpc.InboundFee} */ ( + jspb.Message.getWrapperField(this, proto.lnrpc.InboundFee, 10)); }; -/** @param {number} value */ -proto.lnrpc.PolicyUpdateRequest.prototype.setInboundBaseFeeMsat = function(value) { - jspb.Message.setProto3IntField(this, 10, value); +/** @param {?proto.lnrpc.InboundFee|undefined} value */ +proto.lnrpc.PolicyUpdateRequest.prototype.setInboundFee = function(value) { + jspb.Message.setWrapperField(this, 10, value); }; -/** - * optional int32 inbound_fee_rate_ppm = 11; - * @return {number} - */ -proto.lnrpc.PolicyUpdateRequest.prototype.getInboundFeeRatePpm = function() { - return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 11, 0)); +proto.lnrpc.PolicyUpdateRequest.prototype.clearInboundFee = function() { + this.setInboundFee(undefined); }; -/** @param {number} value */ -proto.lnrpc.PolicyUpdateRequest.prototype.setInboundFeeRatePpm = function(value) { - jspb.Message.setProto3IntField(this, 11, value); +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.lnrpc.PolicyUpdateRequest.prototype.hasInboundFee = function() { + return jspb.Message.getField(this, 10) != null; }; diff --git a/go.mod b/go.mod index 3ba3c8e5e..64f59d676 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/lightninglabs/pool v0.6.5-beta.0.20240604070222-e121aadb3289 github.com/lightninglabs/pool/auctioneerrpc v1.1.2 github.com/lightninglabs/taproot-assets v0.3.3-0.20240625161215-838206d62c99 - github.com/lightningnetwork/lnd v0.18.0-beta.rc3.0.20240625154246-4e968d9b520c + github.com/lightningnetwork/lnd v0.18.0-beta.rc4.0.20240626210328-034df60aaa3d github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/fn v1.1.0 github.com/lightningnetwork/lnd/kvdb v1.4.8 diff --git a/go.sum b/go.sum index 915a68e59..a6819c8a5 100644 --- a/go.sum +++ b/go.sum @@ -1176,8 +1176,8 @@ github.com/lightninglabs/taproot-assets v0.3.3-0.20240625161215-838206d62c99 h1: github.com/lightninglabs/taproot-assets v0.3.3-0.20240625161215-838206d62c99/go.mod h1:KhiaNUkgI3zIYNzfUoEClJjInXt5vScmnLVIvuUzWXY= github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f h1:Pua7+5TcFEJXIIZ1I2YAUapmbcttmLj4TTi786bIi3s= github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= -github.com/lightningnetwork/lnd v0.18.0-beta.rc3.0.20240625154246-4e968d9b520c h1:10hVKzgsnpuzOOgkYAhThUtDiq3fBBJBeZWdir+0ptk= -github.com/lightningnetwork/lnd v0.18.0-beta.rc3.0.20240625154246-4e968d9b520c/go.mod h1:L3IArArdRrWtuw+wNsUlibuGmf/08Odsm/zo3+bPXuM= +github.com/lightningnetwork/lnd v0.18.0-beta.rc4.0.20240626210328-034df60aaa3d h1:G6nnSCeX+sj/1HjZYsqFJT3DfaHkYa+JtcmjKNWUR7g= +github.com/lightningnetwork/lnd v0.18.0-beta.rc4.0.20240626210328-034df60aaa3d/go.mod h1:L3IArArdRrWtuw+wNsUlibuGmf/08Odsm/zo3+bPXuM= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index ae79a675b..f9b30e1ff 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -380,6 +380,568 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, assertAssetChan(t.t, dave, yara, daveFundingAmount, assetID) assertAssetChan(t.t, erin, fabia, erinFundingAmount, assetID) + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) + + // Print initial channel balances. + logBalance(t.t, nodes, assetID, "initial") + + // ------------ + // Test case 1: Send a direct keysend payment from Charlie to Dave. + // ------------ + const keySendAmount = 100 + sendAssetKeySendPayment( + t.t, charlie, dave, keySendAmount, assetID, fn.None[int64](), + ) + logBalance(t.t, nodes, assetID, "after keysend") + + charlieAssetBalance -= keySendAmount + daveAssetBalance += keySendAmount + + // We should be able to send the 100 assets back immediately, because + // there is enough on-chain balance on Dave's side to be able to create + // an HTLC. + sendAssetKeySendPayment( + t.t, dave, charlie, keySendAmount, assetID, fn.None[int64](), + ) + logBalance(t.t, nodes, assetID, "after keysend back") + + charlieAssetBalance += keySendAmount + daveAssetBalance -= keySendAmount + + // We should also be able to do a non-asset (BTC only) keysend payment. + sendKeySendPayment(t.t, charlie, dave, 2000, nil) + logBalance(t.t, nodes, assetID, "after BTC only keysend") + + // ------------ + // Test case 2: Pay a normal invoice from Dave by Charlie, making it + // a direct channel invoice payment with no RFQ SCID present in the + // invoice. + // ------------ + paidAssetAmount := createAndPayNormalInvoice( + t.t, charlie, dave, dave, 20_000, assetID, + ) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= paidAssetAmount + daveAssetBalance += paidAssetAmount + + // We should also be able to do a multi-hop BTC only payment, paying an + // invoice from Erin by Charlie. + createAndPayNormalInvoiceWithBtc(t.t, charlie, erin, 2000) + logBalance(t.t, nodes, assetID, "after BTC only invoice") + + // ------------ + // Test case 3: Pay an asset invoice from Dave by Charlie, making it + // a direct channel invoice payment with an RFQ SCID present in the + // invoice. + // ------------ + const daveInvoiceAssetAmount = 2_000 + invoiceResp := createAssetInvoice( + t.t, charlie, dave, daveInvoiceAssetAmount, assetID, + ) + payInvoiceWithAssets(t.t, charlie, dave, invoiceResp, assetID) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= daveInvoiceAssetAmount + daveAssetBalance += daveInvoiceAssetAmount + + // ------------ + // Test case 4: Pay a normal invoice from Erin by Charlie. + // ------------ + paidAssetAmount = createAndPayNormalInvoice( + t.t, charlie, dave, erin, 20_000, assetID, + ) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= paidAssetAmount + daveAssetBalance += paidAssetAmount + + // ------------ + // Test case 5: Create an asset invoice on Fabia and pay it from + // Charlie. + // ------------ + const fabiaInvoiceAssetAmount1 = 1000 + invoiceResp = createAssetInvoice( + t.t, erin, fabia, fabiaInvoiceAssetAmount1, assetID, + ) + payInvoiceWithAssets(t.t, charlie, dave, invoiceResp, assetID) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= fabiaInvoiceAssetAmount1 + daveAssetBalance += fabiaInvoiceAssetAmount1 + erinAssetBalance -= fabiaInvoiceAssetAmount1 + fabiaAssetBalance += fabiaInvoiceAssetAmount1 + + // ------------ + // Test case 6: Create an asset invoice on Fabia and pay it with just + // BTC from Dave, making sure it ends up being a multipart payment (we + // set the maximum shard size to 80k sat and 15k asset units will be + // more than a single shard). + // ------------ + const fabiaInvoiceAssetAmount2 = 15_000 + invoiceResp = createAssetInvoice( + t.t, erin, fabia, fabiaInvoiceAssetAmount2, assetID, + ) + payInvoiceWithSatoshi(t.t, dave, invoiceResp) + logBalance(t.t, nodes, assetID, "after invoice") + + erinAssetBalance -= fabiaInvoiceAssetAmount2 + fabiaAssetBalance += fabiaInvoiceAssetAmount2 + + // ------------ + // Test case 7: Create an asset invoice on Fabia and pay it with assets + // from Charlie, making sure it ends up being a multipart payment as + // well, with the high amount of asset units to send and the hard coded + // 80k sat max shard size. + // ------------ + const fabiaInvoiceAssetAmount3 = 10_000 + invoiceResp = createAssetInvoice( + t.t, erin, fabia, fabiaInvoiceAssetAmount3, assetID, + ) + payInvoiceWithAssets(t.t, charlie, dave, invoiceResp, assetID) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= fabiaInvoiceAssetAmount3 + daveAssetBalance += fabiaInvoiceAssetAmount3 + erinAssetBalance -= fabiaInvoiceAssetAmount3 + fabiaAssetBalance += fabiaInvoiceAssetAmount3 + + // ------------ + // Test case 8: An invoice payment over two channels that are both asset + // channels. + // ------------ + logBalance(t.t, nodes, assetID, "before asset-to-asset") + + const yaraInvoiceAssetAmount1 = 1000 + invoiceResp = createAssetInvoice( + t.t, dave, yara, yaraInvoiceAssetAmount1, assetID, + ) + payInvoiceWithAssets(t.t, charlie, dave, invoiceResp, assetID) + logBalance(t.t, nodes, assetID, "after asset-to-asset") + + charlieAssetBalance -= yaraInvoiceAssetAmount1 + yaraAssetBalance += yaraInvoiceAssetAmount1 + + // ------------ + // Test case 8: Now we'll close each of the channels, starting with the + // Charlie -> Dave custom channel. + // ------------ + charlieChanPoint := &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespCD.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespCD.Txid, + }, + } + daveChanPoint := &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespDY.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespDY.Txid, + }, + } + erinChanPoint := &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespEF.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespEF.Txid, + }, + } + + t.Logf("Closing Charlie -> Dave channel") + closeAssetChannelAndAssert( + t, net, charlie, dave, charlieChanPoint, assetID, nil, + universeTap, true, true, false, + ) + + t.Logf("Closing Dave -> Yara channel") + closeAssetChannelAndAssert( + t, net, dave, yara, daveChanPoint, assetID, nil, + universeTap, false, true, false, + ) + + t.Logf("Closing Erin -> Fabia channel") + closeAssetChannelAndAssert( + t, net, erin, fabia, erinChanPoint, assetID, nil, + universeTap, true, true, false, + ) + + // We've been tracking the off-chain channel balances all this time, so + // now that we have the assets on-chain again, we can assert them. Due + // to rounding errors that happened when sending multiple shards with + // MPP, we need to do some slight adjustments. + charlieAssetBalance += 2 + daveAssetBalance -= 2 + erinAssetBalance += 4 + fabiaAssetBalance -= 4 + assertAssetBalance(t.t, charlieTap, assetID, charlieAssetBalance) + assertAssetBalance(t.t, daveTap, assetID, daveAssetBalance) + assertAssetBalance(t.t, erinTap, assetID, erinAssetBalance) + assertAssetBalance(t.t, fabiaTap, assetID, fabiaAssetBalance) + assertAssetBalance(t.t, yaraTap, assetID, yaraAssetBalance) + + // ------------ + // Test case 10: We now open a new asset channel and close it again, to + // make sure that a non-existent remote balance is handled correctly. + t.Logf("Opening new asset channel between Charlie and Dave...") + fundRespCD, err = charlieTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: dave.PubKey[:], + FeeRateSatPerVbyte: 5, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded second channel between Charlie and Dave: %v", fundRespCD) + + mineBlocks(t, net, 6, 1) + + // Assert that the proofs for both channels has been uploaded to the + // designated Universe server. + assertUniverseProofExists( + t.t, universeTap, assetID, fundingScriptTreeBytes, fmt.Sprintf( + "%v:%v", fundRespCD.Txid, fundRespCD.OutputIndex, + ), + ) + assertAssetChan(t.t, charlie, dave, fundingAmount, assetID) + + // And let's just close the channel again. + charlieChanPoint = &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespCD.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespCD.Txid, + }, + } + + t.Logf("Closing Charlie -> Dave channel") + closeAssetChannelAndAssert( + t, net, charlie, dave, charlieChanPoint, assetID, nil, + universeTap, false, false, false, + ) + + // Charlie should still have four asset pieces, two with the same size. + assertAssetExists( + t.t, charlieTap, assetID, charlieAssetBalance-fundingAmount, + nil, true, false, false, + ) + assertAssetExists( + t.t, charlieTap, assetID, fundingAmount, nil, true, true, + false, + ) + + // For some reason, the channel funding output of the immediately closed + // channel is still present in the asset DB, even after we import the + // co-op close transaction proof. + // TODO(guggero): Investigate this. The actual number of outputs should + // be two here, and we shouldn't have the extra fundingAmount in the + // balance. + charlieAssetBalance += fundingAmount + assertNumAssetOutputs(t.t, charlieTap, assetID, 3) + + // The asset balances should still remain unchanged. + assertAssetBalance(t.t, charlieTap, assetID, charlieAssetBalance) + assertAssetBalance(t.t, daveTap, assetID, daveAssetBalance) + assertAssetBalance(t.t, erinTap, assetID, erinAssetBalance) + assertAssetBalance(t.t, fabiaTap, assetID, fabiaAssetBalance) +} + +// testCustomChannelsGroupedAsset tests that we can create a network with custom +// channels that use grouped assets and send asset payments over them. +func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, + t *harnessTest) { + + ctxb := context.Background() + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + // Explicitly set the proof courier as Alice (has no other role other + // than proof shuffling), otherwise a hashmail courier will be used. + // For the funding transaction, we're just posting it and don't expect a + // true receiver. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + // The topology we are going for looks like the following: + // + // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia + // | + // | + // [assets] + // | + // v + // Yara + // + // With [assets] being a custom channel and [sats] being a normal, BTC + // only channel. + // All 5 nodes need to be full litd nodes running in integrated mode + // with tapd included. We also need specific flags to be enabled, so we + // create 5 completely new nodes, ignoring the two default nodes that + // are created by the harness. + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + fabia, err := net.NewNode( + t.t, "Fabia", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + yara, err := net.NewNode( + t.t, "Yara", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + // Create the normal channel between Dave and Erin. + t.Logf("Opening normal channel between Dave and Erin...") + channelOp := openChannelAndAssert( + t, net, dave, erin, lntest.OpenChannelParams{ + Amt: 5_000_000, + SatPerVByte: 5, + }, + ) + defer closeChannelAndAssert(t, net, dave, channelOp, false) + + // This is the only public channel, we need everyone to be aware of it. + assertChannelKnown(t.t, charlie, channelOp) + assertChannelKnown(t.t, fabia, channelOp) + + universeTap := newTapClient(t.t, zane) + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + erinTap := newTapClient(t.t, erin) + fabiaTap := newTapClient(t.t, fabia) + yaraTap := newTapClient(t.t, yara) + + groupAssetReq := itest.CopyRequest(&mintrpc.MintAssetRequest{ + Asset: itestAsset, + }) + groupAssetReq.Asset.NewGroupedAsset = true + + // Mint an asset on Charlie and sync all nodes to Charlie as the + // universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{groupAssetReq}, + ) + + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + groupID := cents.GetAssetGroup().GetTweakedGroupKey() + fundingScriptTree := tapchannel.NewFundingScriptTree() + fundingScriptKey := fundingScriptTree.TaprootKey + fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) + t.Logf("Universes synced between all nodes, distributing assets...") + + // We need to send some assets to Dave, so he can fund an asset channel + // with Yara. + const ( + fundingAmount = 50_000 + startAmount = fundingAmount * 2 + ) + daveAddr, err := daveTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: startAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlie.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset units to Dave...", startAmount) + + // Send the assets to Erin. + itest.AssertAddrCreated(t.t, daveTap, cents, daveAddr) + sendResp, err := charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{daveAddr.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, + []uint64{cents.Amount - startAmount, startAmount}, 0, 1, + ) + itest.AssertNonInteractiveRecvComplete(t.t, daveTap, 1) + + // We need to send some assets to Erin, so he can fund an asset channel + // with Fabia. + erinAddr, err := erinTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: startAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlie.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset units to Erin...", startAmount) + + // Send the assets to Erin. + itest.AssertAddrCreated(t.t, erinTap, cents, erinAddr) + sendResp, err = charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{erinAddr.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, + []uint64{cents.Amount - 2*startAmount, startAmount}, 1, 2, + ) + itest.AssertNonInteractiveRecvComplete(t.t, erinTap, 1) + + t.Logf("Opening asset channels...") + + // The first channel we create has a push amount, so Charlie can receive + // payments immediately and not run into the channel reserve issue. + charlieFundingAmount := cents.Amount - 2*startAmount + fundRespCD, err := charlieTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: charlieFundingAmount, + AssetId: assetID, + PeerPubkey: dave.PubKey[:], + FeeRateSatPerVbyte: 5, + PushSat: 1065, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Charlie and Dave: %v", fundRespCD) + + daveFundingAmount := uint64(startAmount) + fundRespDY, err := daveTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: daveFundingAmount, + AssetId: assetID, + PeerPubkey: yara.PubKey[:], + FeeRateSatPerVbyte: 5, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Dave and Yara: %v", fundRespDY) + + erinFundingAmount := uint64(fundingAmount) + fundRespEF, err := erinTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: erinFundingAmount, + AssetId: assetID, + PeerPubkey: fabia.PubKey[:], + FeeRateSatPerVbyte: 5, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Erin and Fabia: %v", fundRespEF) + + // Make sure the pending channel shows up in the list and has the + // custom records set as JSON. + assertPendingChannels(t.t, charlie, assetID, 1, charlieFundingAmount, 0) + assertPendingChannels( + t.t, dave, assetID, 2, daveFundingAmount, charlieFundingAmount, + ) + assertPendingChannels(t.t, erin, assetID, 1, erinFundingAmount, 0) + + // Now that we've looked at the pending channels, let's actually confirm + // all three of them. + mineBlocks(t, net, 6, 3) + + // We'll be tracking the expected asset balances throughout the test, so + // we can assert it after each action. + charlieAssetBalance := charlieFundingAmount + daveAssetBalance := uint64(startAmount) + erinAssetBalance := uint64(startAmount) + fabiaAssetBalance := uint64(0) + yaraAssetBalance := uint64(0) + + // After opening the channels, the asset balance of the funding nodes + // shouldn't have been decreased, since the asset with the funding + // output was imported into the asset DB and should count toward the + // balance. + assertAssetBalance(t.t, charlieTap, assetID, charlieAssetBalance) + assertAssetBalance(t.t, daveTap, assetID, daveAssetBalance) + assertAssetBalance(t.t, erinTap, assetID, erinAssetBalance) + + // There should only be a single asset piece for Charlie, the one in the + // channel. + assertNumAssetOutputs(t.t, charlieTap, assetID, 1) + assertAssetExists( + t.t, charlieTap, assetID, charlieFundingAmount, + fundingScriptKey, false, true, true, + ) + + // Dave should just have one asset piece, since we used the full amount + // for the channel opening. + assertNumAssetOutputs(t.t, daveTap, assetID, 1) + assertAssetExists( + t.t, daveTap, assetID, daveFundingAmount, fundingScriptKey, + false, true, true, + ) + + // Erin should just have two equally sized asset pieces, the change and + // the funding transaction. + assertNumAssetOutputs(t.t, erinTap, assetID, 2) + assertAssetExists( + t.t, erinTap, assetID, startAmount-erinFundingAmount, nil, true, + false, false, + ) + assertAssetExists( + t.t, erinTap, assetID, erinFundingAmount, fundingScriptKey, + false, true, true, + ) + + // Assert that the proofs for both channels has been uploaded to the + // designated Universe server. + assertUniverseGroupProofExists( + t.t, universeTap, groupID, fundingScriptTreeBytes, fmt.Sprintf( + "%v:%v", fundRespCD.Txid, fundRespCD.OutputIndex, + ), + ) + assertUniverseGroupProofExists( + t.t, universeTap, groupID, fundingScriptTreeBytes, fmt.Sprintf( + "%v:%v", fundRespDY.Txid, fundRespDY.OutputIndex, + ), + ) + assertUniverseGroupProofExists( + t.t, universeTap, groupID, fundingScriptTreeBytes, fmt.Sprintf( + "%v:%v", fundRespEF.Txid, fundRespEF.OutputIndex, + ), + ) + + // Make sure the channel shows the correct asset information. + assertAssetChan(t.t, charlie, dave, charlieFundingAmount, assetID) + assertAssetChan(t.t, dave, yara, daveFundingAmount, assetID) + assertAssetChan(t.t, erin, fabia, erinFundingAmount, assetID) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) + // Print initial channel balances. logBalance(t.t, nodes, assetID, "initial") @@ -545,20 +1107,20 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, t.Logf("Closing Charlie -> Dave channel") closeAssetChannelAndAssert( - t, net, charlie, dave, charlieChanPoint, assetID, universeTap, - true, true, + t, net, charlie, dave, charlieChanPoint, assetID, groupID, + universeTap, true, true, true, ) t.Logf("Closing Dave -> Yara channel") closeAssetChannelAndAssert( - t, net, dave, yara, daveChanPoint, assetID, universeTap, false, - true, + t, net, dave, yara, daveChanPoint, assetID, groupID, + universeTap, false, true, true, ) t.Logf("Closing Erin -> Fabia channel") closeAssetChannelAndAssert( - t, net, erin, fabia, erinChanPoint, assetID, universeTap, true, - true, + t, net, erin, fabia, erinChanPoint, assetID, groupID, + universeTap, true, true, true, ) // We've been tracking the off-chain channel balances all this time, so @@ -594,8 +1156,8 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, // Assert that the proofs for both channels has been uploaded to the // designated Universe server. - assertUniverseProofExists( - t.t, universeTap, assetID, fundingScriptTreeBytes, fmt.Sprintf( + assertUniverseGroupProofExists( + t.t, universeTap, groupID, fundingScriptTreeBytes, fmt.Sprintf( "%v:%v", fundRespCD.Txid, fundRespCD.OutputIndex, ), ) @@ -611,8 +1173,8 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, t.Logf("Closing Charlie -> Dave channel") closeAssetChannelAndAssert( - t, net, charlie, dave, charlieChanPoint, assetID, universeTap, - false, false, + t, net, charlie, dave, charlieChanPoint, assetID, groupID, + universeTap, false, false, true, ) // Charlie should still have four asset pieces, two with the same size. @@ -732,9 +1294,7 @@ func testCustomChannelsForceClose(_ context.Context, net *NetworkHarness, // Charlie's balance should reflect that the funding asset was added to // the DB. - assertAssetBalance( - t.t, charlieTap, assetID, uint64(itestAsset.Amount), - ) + assertAssetBalance(t.t, charlieTap, assetID, itestAsset.Amount) // Make sure that Charlie properly uploaded funding proof to the // Universe server. @@ -747,6 +1307,14 @@ func testCustomChannelsForceClose(_ context.Context, net *NetworkHarness, ), ) + // Make sure the channel shows the correct asset information. + assertAssetChan(t.t, charlie, dave, fundingAmount, assetID) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) + // We'll also have dave sync with Charlie+Zane to ensure he has the // proof for the funding output. We sync the transfers as well so he // has all the proofs needed. @@ -897,22 +1465,6 @@ func testCustomChannelsForceClose(_ context.Context, net *NetworkHarness, t.t, charlieSweepTransfer, )) - charlieUTXOs, err := charlieTap.ListUtxos( - ctxb, &taprpc.ListUtxosRequest{}, - ) - require.NoError(t.t, err) - - t.Logf("Charlie UTXOs: %v", toProtoJSON(t.t, charlieUTXOs)) - - daveUTXOs, err := daveTap.ListUtxos( - ctxb, &taprpc.ListUtxosRequest{}, - ) - require.NoError(t.t, err) - - t.Logf("Dave UTXOs: %v", toProtoJSON(t.t, daveUTXOs)) - - // TODO(roasbeef): assert 2 charlie UTXOs - // Both sides should now reflect their updated asset balances. daveBalance := uint64(numPayments * keySendAmount) charlieBalance := itestAsset.Amount - daveBalance @@ -921,26 +1473,12 @@ func testCustomChannelsForceClose(_ context.Context, net *NetworkHarness, // Dave should have a single managed UTXO that shows he has a new asset // UTXO he can use. - err = wait.NoError(func() error { - daveUTXOs, err = daveTap.ListUtxos( - ctxb, &taprpc.ListUtxosRequest{}, - ) - if err != nil { - return err - } - - if len(daveUTXOs.ManagedUtxos) != 1 { - return fmt.Errorf("expected 1 UTXO, got %d", - len(daveUTXOs.ManagedUtxos)) - } - - return nil - }, defaultTimeout) - require.NoError(t.t, err) - - t.Logf("Dave UTXOs: %v", toProtoJSON(t.t, daveUTXOs)) + assertNumAssetUTXOs(t.t, daveTap, 1) + assertNumAssetUTXOs(t.t, charlieTap, 2) } +// testCustomChannelsBreach tests a force close scenario that breaches an old +// state, after both parties have an active asset balance. func testCustomChannelsBreach(_ context.Context, net *NetworkHarness, t *harnessTest) { @@ -984,6 +1522,7 @@ func testCustomChannelsBreach(_ context.Context, net *NetworkHarness, connectAllNodes(t.t, net, nodes) fundAllNodes(t.t, net, nodes) + universeTap := newTapClient(t.t, zane) charlieTap := newTapClient(t.t, charlie) daveTap := newTapClient(t.t, dave) @@ -1025,7 +1564,39 @@ func testCustomChannelsBreach(_ context.Context, net *NetworkHarness, // With the channel open, mine a block to confirm it. mineBlocks(t, net, 6, 1) - time.Sleep(time.Second * 1) + // A transfer for the funding transaction should be found in Charlie's + // DB. + fundingTxid, err := chainhash.NewHashFromStr(assetFundResp.Txid) + require.NoError(t.t, err) + assetFundingTransfer := locateAssetTransfers( + t.t, charlieTap, *fundingTxid, + ) + + t.Logf("Channel funding transfer: %v", + toProtoJSON(t.t, assetFundingTransfer)) + + // Charlie's balance should reflect that the funding asset was added to + // the DB. + assertAssetBalance(t.t, charlieTap, assetID, itestAsset.Amount) + + // Make sure that Charlie properly uploaded funding proof to the + // Universe server. + fundingScriptTree := tapchannel.NewFundingScriptTree() + fundingScriptKey := fundingScriptTree.TaprootKey + fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() + assertUniverseProofExists( + t.t, universeTap, assetID, fundingScriptTreeBytes, fmt.Sprintf( + "%v:%v", assetFundResp.Txid, assetFundResp.OutputIndex, + ), + ) + + // Make sure the channel shows the correct asset information. + assertAssetChan(t.t, charlie, dave, fundingAmount, assetID) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) // Next, we'll make keysend payments from Charlie to Dave. we'll use // this to reach a state where both parties have funds in the channel. @@ -1128,8 +1699,10 @@ func assertNumAssetUTXOs(t *testing.T, tapdClient *tapClient, ctxb := context.Background() + var clientUTXOs *taprpc.ListUtxosResponse err := wait.NoError(func() error { - clientUTXOs, err := tapdClient.ListUtxos( + var err error + clientUTXOs, err = tapdClient.ListUtxos( ctxb, &taprpc.ListUtxosRequest{}, ) if err != nil { @@ -1143,21 +1716,8 @@ func assertNumAssetUTXOs(t *testing.T, tapdClient *tapClient, return nil }, defaultTimeout) - - clientUTXOs, err2 := tapdClient.ListUtxos( - ctxb, &taprpc.ListUtxosRequest{}, - ) - require.NoError(t, err2) - - if err != nil { - t.Logf("wrong amount of UTXOs, got %d, expected %d: %v", - len(clientUTXOs.ManagedUtxos), numUTXOs, - toProtoJSON(t, clientUTXOs)) - - t.Fatalf("failed to assert UTXOs: %v", err) - - return nil - } + require.NoErrorf(t, err, "failed to assert UTXOs: %v, last state: %v", + err, clientUTXOs) return clientUTXOs } @@ -1225,6 +1785,50 @@ func syncUniverses(t *testing.T, universe *tapClient, nodes ...*HarnessNode) { } } +func assertUniverseGroupProofExists(t *testing.T, universe *tapClient, + groupID, scriptKey []byte, outpoint string) *taprpc.Asset { + + t.Logf("Asserting proof outpoint=%v, script_key=%x", outpoint, + scriptKey) + + req := &universerpc.UniverseKey{ + Id: &universerpc.ID{ + Id: &universerpc.ID_GroupKey{ + GroupKey: groupID, + }, + ProofType: universerpc.ProofType_PROOF_TYPE_TRANSFER, + }, + LeafKey: &universerpc.AssetKey{ + Outpoint: &universerpc.AssetKey_OpStr{ + OpStr: outpoint, + }, + ScriptKey: &universerpc.AssetKey_ScriptKeyBytes{ + ScriptKeyBytes: scriptKey, + }, + }, + } + + ctxb := context.Background() + var proofResp *universerpc.AssetProofResponse + err := wait.NoError(func() error { + var pErr error + proofResp, pErr = universe.QueryProof(ctxb, req) + return pErr + }, defaultTimeout) + require.NoError( + t, err, "%v: outpoint=%v, script_key=%x", err, outpoint, + scriptKey, + ) + require.Equal( + t, proofResp.AssetLeaf.Asset.AssetGroup.TweakedGroupKey, + groupID, + ) + a := proofResp.AssetLeaf.Asset + t.Logf("Proof found for scriptKey=%x, amount=%d", a.ScriptKey, a.Amount) + + return a +} + func assertUniverseProofExists(t *testing.T, universe *tapClient, assetID, scriptKey []byte, outpoint string) *taprpc.Asset { @@ -1757,8 +2361,8 @@ func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, func closeAssetChannelAndAssert(t *harnessTest, net *NetworkHarness, local, remote *HarnessNode, chanPoint *lnrpc.ChannelPoint, - assetID []byte, universeTap *tapClient, remoteBtcBalance, - remoteAssetBalance bool) { + assetID, groupID []byte, universeTap *tapClient, remoteBtcBalance, + remoteAssetBalance, groupAsset bool) { closeStream, _, err := t.lndHarness.CloseChannel( local, chanPoint, false, @@ -1888,11 +2492,22 @@ func closeAssetChannelAndAssert(t *harnessTest, net *NetworkHarness, require.Equal(t.t, hex.EncodeToString(assetID), assetIDStr) - a := assertUniverseProofExists( - t.t, universeTap, assetID, scriptKeyBytes, fmt.Sprintf( - "%v:%v", closeTxid, localAssetIndex, - ), - ) + var a *taprpc.Asset + if groupAsset { + a = assertUniverseGroupProofExists( + t.t, universeTap, groupID, scriptKeyBytes, + fmt.Sprintf( + "%v:%v", closeTxid, localAssetIndex, + ), + ) + } else { + a = assertUniverseProofExists( + t.t, universeTap, assetID, scriptKeyBytes, + fmt.Sprintf( + "%v:%v", closeTxid, localAssetIndex, + ), + ) + } localTapd := newTapClient(t.t, local) @@ -1927,11 +2542,22 @@ func closeAssetChannelAndAssert(t *harnessTest, net *NetworkHarness, require.Equal(t.t, hex.EncodeToString(assetID), assetIDStr) - a := assertUniverseProofExists( - t.t, universeTap, assetID, scriptKeyBytes, fmt.Sprintf( - "%v:%v", closeTxid, remoteAssetIndex, - ), - ) + var a *taprpc.Asset + if groupAsset { + a = assertUniverseGroupProofExists( + t.t, universeTap, groupID, scriptKeyBytes, + fmt.Sprintf( + "%v:%v", closeTxid, remoteAssetIndex, + ), + ) + } else { + a = assertUniverseProofExists( + t.t, universeTap, assetID, scriptKeyBytes, + fmt.Sprintf( + "%v:%v", closeTxid, remoteAssetIndex, + ), + ) + } remoteTapd := newTapClient(t.t, remote) diff --git a/itest/litd_test_list_on_test.go b/itest/litd_test_list_on_test.go index 0c1d32610..159277bb9 100644 --- a/itest/litd_test_list_on_test.go +++ b/itest/litd_test_list_on_test.go @@ -24,6 +24,10 @@ var allTestCases = []*testCase{ name: "test custom channels", test: testCustomChannels, }, + { + name: "test custom channels grouped asset", + test: testCustomChannelsGroupedAsset, + }, { name: "test custom channels force close", test: testCustomChannelsForceClose, diff --git a/itest/network_harness.go b/itest/network_harness.go index 97884cf45..5cbe8b8c4 100644 --- a/itest/network_harness.go +++ b/itest/network_harness.go @@ -1180,6 +1180,54 @@ func (n *NetworkHarness) AssertChannelExists(node *HarnessNode, }, lntest.DefaultTimeout) } +// AssertNodeKnown makes sure the given node knows about the target node in the +// network graph. +func (n *NetworkHarness) AssertNodeKnown(node, target *HarnessNode) error { + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, wait.DefaultTimeout) + defer cancel() + + req := &lnrpc.NodeInfoRequest{ + PubKey: hex.EncodeToString( + target.PubKey[:], + ), + } + return wait.NoError(func() error { + info, err := node.GetNodeInfo(ctxt, req) + if err != nil { + return err + } + + if info.Node == nil { + return fmt.Errorf("node %x has no info about %x", + node.PubKey[:], target.PubKey[:]) + } + + // TODO(guggero): Uncomment this once the lnd bug of the node + // announcement timing is fixed: + // https://github.com/lightningnetwork/lnd/issues/8870 + // + // if len(info.Node.Features) == 0 { + // return fmt.Errorf("node %x has no features for %x", + // node.PubKey[:], target.PubKey[:]) + // } + // + // _, optional := info.Node.Features[uint32( + // lnwire.TLVOnionPayloadOptional, + // )] + // _, required := info.Node.Features[uint32( + // lnwire.TLVOnionPayloadRequired, + // )] + // if !optional && !required { + // return fmt.Errorf("node %x has no onion payload "+ + // "features for %x", node.PubKey[:], + // target.PubKey[:]) + // } + + return nil + }, lntest.DefaultTimeout) +} + // DumpLogs reads the current logs generated by the passed node, and returns // the logs as a single string. This function is useful for examining the logs // of a particular node in the case of a test failure. diff --git a/proto/lnd.proto b/proto/lnd.proto index f6de9de00..7a663013d 100644 --- a/proto/lnd.proto +++ b/proto/lnd.proto @@ -4369,6 +4369,16 @@ message FeeReportResponse { uint64 month_fee_sum = 4 [jstype = JS_STRING]; } +message InboundFee { + // The inbound base fee charged regardless of the number of milli-satoshis + // received in the channel. By default, only negative values are accepted. + int32 base_fee_msat = 1; + + // The effective inbound fee rate in micro-satoshis (parts per million). + // By default, only negative values are accepted. + int32 fee_rate_ppm = 2; +} + message PolicyUpdateRequest { oneof scope { // If set, then this update applies to all currently active channels. @@ -4402,8 +4412,9 @@ message PolicyUpdateRequest { // If true, min_htlc_msat is applied. bool min_htlc_msat_specified = 8; - int32 inbound_base_fee_msat = 10; - int32 inbound_fee_rate_ppm = 11; + // Optional inbound fee. If unset, the previously set value will be + // retained [EXPERIMENTAL]. + InboundFee inbound_fee = 10; } enum UpdateFailure {