diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 342dea06f..41fc56d9c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -114,6 +114,9 @@ jobs: working-directory: ./app run: yarn + - name: run check + run: make rpc-js-compile && make protos-check + - name: compile rpc for golang run: make rpc diff --git a/Makefile b/Makefile index c24f728b3..9bc20e968 100644 --- a/Makefile +++ b/Makefile @@ -228,6 +228,10 @@ protos-check: protos @$(call print, "Verifying compiled protos.") if test -n "$$(git describe --dirty | grep dirty)"; then echo "Protos not properly formatted or not compiled with correct version"; git status; git diff; exit 1; fi +rpc-js-compile: + @$(call print, "Compiling JSON/WASM stubs.") + GOOS=js GOARCH=wasm $(GOBUILD) $(PKG)/litrpc + clean: @$(call print, "Cleaning source.$(NC)") $(RM) ./litcli-debug diff --git a/app/src/types/generated/lit-sessions_pb.d.ts b/app/src/types/generated/lit-sessions_pb.d.ts index 4b6a00d76..c9ce8cc50 100644 --- a/app/src/types/generated/lit-sessions_pb.d.ts +++ b/app/src/types/generated/lit-sessions_pb.d.ts @@ -96,6 +96,11 @@ export namespace AddSessionResponse { } export class Session extends jspb.Message { + getId(): Uint8Array | string; + getId_asU8(): Uint8Array; + getId_asB64(): string; + setId(value: Uint8Array | string): void; + getLabel(): string; setLabel(value: string): void; @@ -143,6 +148,11 @@ export class Session extends jspb.Message { getAccountId(): string; setAccountId(value: string): void; + getAutopilotFeatureInfoMap(): jspb.Map; + clearAutopilotFeatureInfoMap(): void; + getRevokedAt(): string; + setRevokedAt(value: string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): Session.AsObject; static toObject(includeInstance: boolean, msg: Session): Session.AsObject; @@ -155,6 +165,7 @@ export class Session extends jspb.Message { export namespace Session { export type AsObject = { + id: Uint8Array | string, label: string, sessionState: SessionStateMap[keyof SessionStateMap], sessionType: SessionTypeMap[keyof SessionTypeMap], @@ -168,6 +179,8 @@ export namespace Session { createdAt: string, macaroonRecipe?: MacaroonRecipe.AsObject, accountId: string, + autopilotFeatureInfoMap: Array<[string, RulesMap.AsObject]>, + revokedAt: string, } } @@ -275,11 +288,340 @@ export namespace RevokeSessionResponse { } } +export class RulesMap extends jspb.Message { + getRulesMap(): jspb.Map; + clearRulesMap(): void; + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): RulesMap.AsObject; + static toObject(includeInstance: boolean, msg: RulesMap): RulesMap.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: RulesMap, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): RulesMap; + static deserializeBinaryFromReader(message: RulesMap, reader: jspb.BinaryReader): RulesMap; +} + +export namespace RulesMap { + export type AsObject = { + rulesMap: Array<[string, RuleValue.AsObject]>, + } +} + +export class RuleValue extends jspb.Message { + hasRateLimit(): boolean; + clearRateLimit(): void; + getRateLimit(): RateLimit | undefined; + setRateLimit(value?: RateLimit): void; + + hasChanPolicyBounds(): boolean; + clearChanPolicyBounds(): void; + getChanPolicyBounds(): ChannelPolicyBounds | undefined; + setChanPolicyBounds(value?: ChannelPolicyBounds): void; + + hasHistoryLimit(): boolean; + clearHistoryLimit(): void; + getHistoryLimit(): HistoryLimit | undefined; + setHistoryLimit(value?: HistoryLimit): void; + + hasOffChainBudget(): boolean; + clearOffChainBudget(): void; + getOffChainBudget(): OffChainBudget | undefined; + setOffChainBudget(value?: OffChainBudget): void; + + hasOnChainBudget(): boolean; + clearOnChainBudget(): void; + getOnChainBudget(): OnChainBudget | undefined; + setOnChainBudget(value?: OnChainBudget): void; + + hasSendToSelf(): boolean; + clearSendToSelf(): void; + getSendToSelf(): SendToSelf | undefined; + setSendToSelf(value?: SendToSelf): void; + + hasChannelRestrict(): boolean; + clearChannelRestrict(): void; + getChannelRestrict(): ChannelRestrict | undefined; + setChannelRestrict(value?: ChannelRestrict): void; + + hasPeerRestrict(): boolean; + clearPeerRestrict(): void; + getPeerRestrict(): PeerRestrict | undefined; + setPeerRestrict(value?: PeerRestrict): void; + + getValueCase(): RuleValue.ValueCase; + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): RuleValue.AsObject; + static toObject(includeInstance: boolean, msg: RuleValue): RuleValue.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: RuleValue, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): RuleValue; + static deserializeBinaryFromReader(message: RuleValue, reader: jspb.BinaryReader): RuleValue; +} + +export namespace RuleValue { + export type AsObject = { + rateLimit?: RateLimit.AsObject, + chanPolicyBounds?: ChannelPolicyBounds.AsObject, + historyLimit?: HistoryLimit.AsObject, + offChainBudget?: OffChainBudget.AsObject, + onChainBudget?: OnChainBudget.AsObject, + sendToSelf?: SendToSelf.AsObject, + channelRestrict?: ChannelRestrict.AsObject, + peerRestrict?: PeerRestrict.AsObject, + } + + export enum ValueCase { + VALUE_NOT_SET = 0, + RATE_LIMIT = 1, + CHAN_POLICY_BOUNDS = 2, + HISTORY_LIMIT = 3, + OFF_CHAIN_BUDGET = 4, + ON_CHAIN_BUDGET = 5, + SEND_TO_SELF = 6, + CHANNEL_RESTRICT = 7, + PEER_RESTRICT = 8, + } +} + +export class RateLimit extends jspb.Message { + hasReadLimit(): boolean; + clearReadLimit(): void; + getReadLimit(): Rate | undefined; + setReadLimit(value?: Rate): void; + + hasWriteLimit(): boolean; + clearWriteLimit(): void; + getWriteLimit(): Rate | undefined; + setWriteLimit(value?: Rate): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): RateLimit.AsObject; + static toObject(includeInstance: boolean, msg: RateLimit): RateLimit.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: RateLimit, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): RateLimit; + static deserializeBinaryFromReader(message: RateLimit, reader: jspb.BinaryReader): RateLimit; +} + +export namespace RateLimit { + export type AsObject = { + readLimit?: Rate.AsObject, + writeLimit?: Rate.AsObject, + } +} + +export class Rate extends jspb.Message { + getIterations(): number; + setIterations(value: number): void; + + getNumHours(): number; + setNumHours(value: number): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): Rate.AsObject; + static toObject(includeInstance: boolean, msg: Rate): Rate.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: Rate, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): Rate; + static deserializeBinaryFromReader(message: Rate, reader: jspb.BinaryReader): Rate; +} + +export namespace Rate { + export type AsObject = { + iterations: number, + numHours: number, + } +} + +export class HistoryLimit extends jspb.Message { + getStartTime(): string; + setStartTime(value: string): void; + + getDuration(): string; + setDuration(value: string): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): HistoryLimit.AsObject; + static toObject(includeInstance: boolean, msg: HistoryLimit): HistoryLimit.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: HistoryLimit, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): HistoryLimit; + static deserializeBinaryFromReader(message: HistoryLimit, reader: jspb.BinaryReader): HistoryLimit; +} + +export namespace HistoryLimit { + export type AsObject = { + startTime: string, + duration: string, + } +} + +export class ChannelPolicyBounds extends jspb.Message { + getMinBaseMsat(): string; + setMinBaseMsat(value: string): void; + + getMaxBaseMsat(): string; + setMaxBaseMsat(value: string): void; + + getMinRatePpm(): number; + setMinRatePpm(value: number): void; + + getMaxRatePpm(): number; + setMaxRatePpm(value: number): void; + + getMinCltvDelta(): number; + setMinCltvDelta(value: number): void; + + getMaxCltvDelta(): number; + setMaxCltvDelta(value: number): void; + + getMinHtlcMsat(): string; + setMinHtlcMsat(value: string): void; + + getMaxHtlcMsat(): string; + setMaxHtlcMsat(value: string): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): ChannelPolicyBounds.AsObject; + static toObject(includeInstance: boolean, msg: ChannelPolicyBounds): ChannelPolicyBounds.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: ChannelPolicyBounds, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): ChannelPolicyBounds; + static deserializeBinaryFromReader(message: ChannelPolicyBounds, reader: jspb.BinaryReader): ChannelPolicyBounds; +} + +export namespace ChannelPolicyBounds { + export type AsObject = { + minBaseMsat: string, + maxBaseMsat: string, + minRatePpm: number, + maxRatePpm: number, + minCltvDelta: number, + maxCltvDelta: number, + minHtlcMsat: string, + maxHtlcMsat: string, + } +} + +export class OffChainBudget extends jspb.Message { + getMaxAmtMsat(): string; + setMaxAmtMsat(value: string): void; + + getMaxFeesMsat(): string; + setMaxFeesMsat(value: string): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): OffChainBudget.AsObject; + static toObject(includeInstance: boolean, msg: OffChainBudget): OffChainBudget.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: OffChainBudget, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): OffChainBudget; + static deserializeBinaryFromReader(message: OffChainBudget, reader: jspb.BinaryReader): OffChainBudget; +} + +export namespace OffChainBudget { + export type AsObject = { + maxAmtMsat: string, + maxFeesMsat: string, + } +} + +export class OnChainBudget extends jspb.Message { + getAbsoluteAmtSats(): string; + setAbsoluteAmtSats(value: string): void; + + getMaxSatPerVByte(): string; + setMaxSatPerVByte(value: string): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): OnChainBudget.AsObject; + static toObject(includeInstance: boolean, msg: OnChainBudget): OnChainBudget.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: OnChainBudget, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): OnChainBudget; + static deserializeBinaryFromReader(message: OnChainBudget, reader: jspb.BinaryReader): OnChainBudget; +} + +export namespace OnChainBudget { + export type AsObject = { + absoluteAmtSats: string, + maxSatPerVByte: string, + } +} + +export class SendToSelf extends jspb.Message { + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): SendToSelf.AsObject; + static toObject(includeInstance: boolean, msg: SendToSelf): SendToSelf.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: SendToSelf, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): SendToSelf; + static deserializeBinaryFromReader(message: SendToSelf, reader: jspb.BinaryReader): SendToSelf; +} + +export namespace SendToSelf { + export type AsObject = { + } +} + +export class ChannelRestrict extends jspb.Message { + clearChannelIdsList(): void; + getChannelIdsList(): Array; + setChannelIdsList(value: Array): void; + addChannelIds(value: string, index?: number): string; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): ChannelRestrict.AsObject; + static toObject(includeInstance: boolean, msg: ChannelRestrict): ChannelRestrict.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: ChannelRestrict, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): ChannelRestrict; + static deserializeBinaryFromReader(message: ChannelRestrict, reader: jspb.BinaryReader): ChannelRestrict; +} + +export namespace ChannelRestrict { + export type AsObject = { + channelIdsList: Array, + } +} + +export class PeerRestrict extends jspb.Message { + clearPeerIdsList(): void; + getPeerIdsList(): Array; + setPeerIdsList(value: Array): void; + addPeerIds(value: string, index?: number): string; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): PeerRestrict.AsObject; + static toObject(includeInstance: boolean, msg: PeerRestrict): PeerRestrict.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: PeerRestrict, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): PeerRestrict; + static deserializeBinaryFromReader(message: PeerRestrict, reader: jspb.BinaryReader): PeerRestrict; +} + +export namespace PeerRestrict { + export type AsObject = { + peerIdsList: Array, + } +} + export interface SessionTypeMap { TYPE_MACAROON_READONLY: 0; TYPE_MACAROON_ADMIN: 1; TYPE_MACAROON_CUSTOM: 2; TYPE_UI_PASSWORD: 3; + TYPE_AUTOPILOT: 4; TYPE_MACAROON_ACCOUNT: 5; } diff --git a/app/src/types/generated/lit-sessions_pb.js b/app/src/types/generated/lit-sessions_pb.js index 82730227b..e09f51593 100644 --- a/app/src/types/generated/lit-sessions_pb.js +++ b/app/src/types/generated/lit-sessions_pb.js @@ -16,12 +16,23 @@ var global = Function('return this')(); goog.exportSymbol('proto.litrpc.AddSessionRequest', null, global); goog.exportSymbol('proto.litrpc.AddSessionResponse', null, global); +goog.exportSymbol('proto.litrpc.ChannelPolicyBounds', null, global); +goog.exportSymbol('proto.litrpc.ChannelRestrict', null, global); +goog.exportSymbol('proto.litrpc.HistoryLimit', null, global); goog.exportSymbol('proto.litrpc.ListSessionsRequest', null, global); goog.exportSymbol('proto.litrpc.ListSessionsResponse', null, global); goog.exportSymbol('proto.litrpc.MacaroonPermission', null, global); goog.exportSymbol('proto.litrpc.MacaroonRecipe', null, global); +goog.exportSymbol('proto.litrpc.OffChainBudget', null, global); +goog.exportSymbol('proto.litrpc.OnChainBudget', null, global); +goog.exportSymbol('proto.litrpc.PeerRestrict', null, global); +goog.exportSymbol('proto.litrpc.Rate', null, global); +goog.exportSymbol('proto.litrpc.RateLimit', null, global); goog.exportSymbol('proto.litrpc.RevokeSessionRequest', null, global); goog.exportSymbol('proto.litrpc.RevokeSessionResponse', null, global); +goog.exportSymbol('proto.litrpc.RuleValue', null, global); +goog.exportSymbol('proto.litrpc.RulesMap', null, global); +goog.exportSymbol('proto.litrpc.SendToSelf', null, global); goog.exportSymbol('proto.litrpc.Session', null, global); goog.exportSymbol('proto.litrpc.SessionState', null, global); goog.exportSymbol('proto.litrpc.SessionType', null, global); @@ -732,6 +743,7 @@ proto.litrpc.Session.prototype.toObject = function(opt_includeInstance) { */ proto.litrpc.Session.toObject = function(includeInstance, msg) { var f, obj = { + id: msg.getId_asB64(), label: jspb.Message.getFieldWithDefault(msg, 1, ""), sessionState: jspb.Message.getFieldWithDefault(msg, 2, 0), sessionType: jspb.Message.getFieldWithDefault(msg, 3, 0), @@ -744,7 +756,9 @@ proto.litrpc.Session.toObject = function(includeInstance, msg) { remotePublicKey: msg.getRemotePublicKey_asB64(), createdAt: jspb.Message.getFieldWithDefault(msg, 11, "0"), macaroonRecipe: (f = msg.getMacaroonRecipe()) && proto.litrpc.MacaroonRecipe.toObject(includeInstance, f), - accountId: jspb.Message.getFieldWithDefault(msg, 13, "") + accountId: jspb.Message.getFieldWithDefault(msg, 13, ""), + autopilotFeatureInfoMap: (f = msg.getAutopilotFeatureInfoMap()) ? f.toObject(includeInstance, proto.litrpc.RulesMap.toObject) : [], + revokedAt: jspb.Message.getFieldWithDefault(msg, 16, "0") }; if (includeInstance) { @@ -781,6 +795,10 @@ proto.litrpc.Session.deserializeBinaryFromReader = function(msg, reader) { } var field = reader.getFieldNumber(); switch (field) { + case 14: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setId(value); + break; case 1: var value = /** @type {string} */ (reader.readString()); msg.setLabel(value); @@ -834,6 +852,16 @@ proto.litrpc.Session.deserializeBinaryFromReader = function(msg, reader) { var value = /** @type {string} */ (reader.readString()); msg.setAccountId(value); break; + case 15: + var value = msg.getAutopilotFeatureInfoMap(); + reader.readMessage(value, function(message, reader) { + jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readMessage, proto.litrpc.RulesMap.deserializeBinaryFromReader, ""); + }); + break; + case 16: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setRevokedAt(value); + break; default: reader.skipField(); break; @@ -863,6 +891,13 @@ proto.litrpc.Session.prototype.serializeBinary = function() { */ proto.litrpc.Session.serializeBinaryToWriter = function(message, writer) { var f = undefined; + f = message.getId_asU8(); + if (f.length > 0) { + writer.writeBytes( + 14, + f + ); + } f = message.getLabel(); if (f.length > 0) { writer.writeString( @@ -955,6 +990,56 @@ proto.litrpc.Session.serializeBinaryToWriter = function(message, writer) { f ); } + f = message.getAutopilotFeatureInfoMap(true); + if (f && f.getLength() > 0) { + f.serializeBinary(15, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeMessage, proto.litrpc.RulesMap.serializeBinaryToWriter); + } + f = message.getRevokedAt(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 16, + f + ); + } +}; + + +/** + * optional bytes id = 14; + * @return {!(string|Uint8Array)} + */ +proto.litrpc.Session.prototype.getId = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 14, "")); +}; + + +/** + * optional bytes id = 14; + * This is a type-conversion wrapper around `getId()` + * @return {string} + */ +proto.litrpc.Session.prototype.getId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getId())); +}; + + +/** + * optional bytes id = 14; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getId()` + * @return {!Uint8Array} + */ +proto.litrpc.Session.prototype.getId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getId())); +}; + + +/** @param {!(string|Uint8Array)} value */ +proto.litrpc.Session.prototype.setId = function(value) { + jspb.Message.setProto3BytesField(this, 14, value); }; @@ -1242,6 +1327,39 @@ proto.litrpc.Session.prototype.setAccountId = function(value) { }; +/** + * map autopilot_feature_info = 15; + * @param {boolean=} opt_noLazyCreate Do not create the map if + * empty, instead returning `undefined` + * @return {!jspb.Map} + */ +proto.litrpc.Session.prototype.getAutopilotFeatureInfoMap = function(opt_noLazyCreate) { + return /** @type {!jspb.Map} */ ( + jspb.Message.getMapField(this, 15, opt_noLazyCreate, + proto.litrpc.RulesMap)); +}; + + +proto.litrpc.Session.prototype.clearAutopilotFeatureInfoMap = function() { + this.getAutopilotFeatureInfoMap().clear(); +}; + + +/** + * optional uint64 revoked_at = 16; + * @return {string} + */ +proto.litrpc.Session.prototype.getRevokedAt = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 16, "0")); +}; + + +/** @param {string} value */ +proto.litrpc.Session.prototype.setRevokedAt = function(value) { + jspb.Message.setProto3StringIntField(this, 16, value); +}; + + /** * Generated by JsPbCodeGenerator. @@ -2017,6 +2135,2301 @@ proto.litrpc.RevokeSessionResponse.serializeBinaryToWriter = function(message, w }; + +/** + * 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.litrpc.RulesMap = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.litrpc.RulesMap, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.litrpc.RulesMap.displayName = 'proto.litrpc.RulesMap'; +} + + +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.litrpc.RulesMap.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.RulesMap.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.litrpc.RulesMap} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.RulesMap.toObject = function(includeInstance, msg) { + var f, obj = { + rulesMap: (f = msg.getRulesMap()) ? f.toObject(includeInstance, proto.litrpc.RuleValue.toObject) : [] + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.litrpc.RulesMap} + */ +proto.litrpc.RulesMap.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.RulesMap; + return proto.litrpc.RulesMap.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.RulesMap} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.RulesMap} + */ +proto.litrpc.RulesMap.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = msg.getRulesMap(); + reader.readMessage(value, function(message, reader) { + jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readMessage, proto.litrpc.RuleValue.deserializeBinaryFromReader, ""); + }); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.RulesMap.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.RulesMap.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.litrpc.RulesMap} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.RulesMap.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getRulesMap(true); + if (f && f.getLength() > 0) { + f.serializeBinary(1, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeMessage, proto.litrpc.RuleValue.serializeBinaryToWriter); + } +}; + + +/** + * map rules = 1; + * @param {boolean=} opt_noLazyCreate Do not create the map if + * empty, instead returning `undefined` + * @return {!jspb.Map} + */ +proto.litrpc.RulesMap.prototype.getRulesMap = function(opt_noLazyCreate) { + return /** @type {!jspb.Map} */ ( + jspb.Message.getMapField(this, 1, opt_noLazyCreate, + proto.litrpc.RuleValue)); +}; + + +proto.litrpc.RulesMap.prototype.clearRulesMap = function() { + this.getRulesMap().clear(); +}; + + + +/** + * 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.litrpc.RuleValue = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, proto.litrpc.RuleValue.oneofGroups_); +}; +goog.inherits(proto.litrpc.RuleValue, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.litrpc.RuleValue.displayName = 'proto.litrpc.RuleValue'; +} +/** + * Oneof group definitions for this message. Each group defines the field + * numbers belonging to that group. When of these fields' value is set, all + * other fields in the group are cleared. During deserialization, if multiple + * fields are encountered for a group, only the last value seen will be kept. + * @private {!Array>} + * @const + */ +proto.litrpc.RuleValue.oneofGroups_ = [[1,2,3,4,5,6,7,8]]; + +/** + * @enum {number} + */ +proto.litrpc.RuleValue.ValueCase = { + VALUE_NOT_SET: 0, + RATE_LIMIT: 1, + CHAN_POLICY_BOUNDS: 2, + HISTORY_LIMIT: 3, + OFF_CHAIN_BUDGET: 4, + ON_CHAIN_BUDGET: 5, + SEND_TO_SELF: 6, + CHANNEL_RESTRICT: 7, + PEER_RESTRICT: 8 +}; + +/** + * @return {proto.litrpc.RuleValue.ValueCase} + */ +proto.litrpc.RuleValue.prototype.getValueCase = function() { + return /** @type {proto.litrpc.RuleValue.ValueCase} */(jspb.Message.computeOneofCase(this, proto.litrpc.RuleValue.oneofGroups_[0])); +}; + + + +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.litrpc.RuleValue.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.RuleValue.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.litrpc.RuleValue} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.RuleValue.toObject = function(includeInstance, msg) { + var f, obj = { + rateLimit: (f = msg.getRateLimit()) && proto.litrpc.RateLimit.toObject(includeInstance, f), + chanPolicyBounds: (f = msg.getChanPolicyBounds()) && proto.litrpc.ChannelPolicyBounds.toObject(includeInstance, f), + historyLimit: (f = msg.getHistoryLimit()) && proto.litrpc.HistoryLimit.toObject(includeInstance, f), + offChainBudget: (f = msg.getOffChainBudget()) && proto.litrpc.OffChainBudget.toObject(includeInstance, f), + onChainBudget: (f = msg.getOnChainBudget()) && proto.litrpc.OnChainBudget.toObject(includeInstance, f), + sendToSelf: (f = msg.getSendToSelf()) && proto.litrpc.SendToSelf.toObject(includeInstance, f), + channelRestrict: (f = msg.getChannelRestrict()) && proto.litrpc.ChannelRestrict.toObject(includeInstance, f), + peerRestrict: (f = msg.getPeerRestrict()) && proto.litrpc.PeerRestrict.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.litrpc.RuleValue} + */ +proto.litrpc.RuleValue.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.RuleValue; + return proto.litrpc.RuleValue.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.RuleValue} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.RuleValue} + */ +proto.litrpc.RuleValue.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.litrpc.RateLimit; + reader.readMessage(value,proto.litrpc.RateLimit.deserializeBinaryFromReader); + msg.setRateLimit(value); + break; + case 2: + var value = new proto.litrpc.ChannelPolicyBounds; + reader.readMessage(value,proto.litrpc.ChannelPolicyBounds.deserializeBinaryFromReader); + msg.setChanPolicyBounds(value); + break; + case 3: + var value = new proto.litrpc.HistoryLimit; + reader.readMessage(value,proto.litrpc.HistoryLimit.deserializeBinaryFromReader); + msg.setHistoryLimit(value); + break; + case 4: + var value = new proto.litrpc.OffChainBudget; + reader.readMessage(value,proto.litrpc.OffChainBudget.deserializeBinaryFromReader); + msg.setOffChainBudget(value); + break; + case 5: + var value = new proto.litrpc.OnChainBudget; + reader.readMessage(value,proto.litrpc.OnChainBudget.deserializeBinaryFromReader); + msg.setOnChainBudget(value); + break; + case 6: + var value = new proto.litrpc.SendToSelf; + reader.readMessage(value,proto.litrpc.SendToSelf.deserializeBinaryFromReader); + msg.setSendToSelf(value); + break; + case 7: + var value = new proto.litrpc.ChannelRestrict; + reader.readMessage(value,proto.litrpc.ChannelRestrict.deserializeBinaryFromReader); + msg.setChannelRestrict(value); + break; + case 8: + var value = new proto.litrpc.PeerRestrict; + reader.readMessage(value,proto.litrpc.PeerRestrict.deserializeBinaryFromReader); + msg.setPeerRestrict(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.RuleValue.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.RuleValue.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.litrpc.RuleValue} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.RuleValue.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getRateLimit(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.litrpc.RateLimit.serializeBinaryToWriter + ); + } + f = message.getChanPolicyBounds(); + if (f != null) { + writer.writeMessage( + 2, + f, + proto.litrpc.ChannelPolicyBounds.serializeBinaryToWriter + ); + } + f = message.getHistoryLimit(); + if (f != null) { + writer.writeMessage( + 3, + f, + proto.litrpc.HistoryLimit.serializeBinaryToWriter + ); + } + f = message.getOffChainBudget(); + if (f != null) { + writer.writeMessage( + 4, + f, + proto.litrpc.OffChainBudget.serializeBinaryToWriter + ); + } + f = message.getOnChainBudget(); + if (f != null) { + writer.writeMessage( + 5, + f, + proto.litrpc.OnChainBudget.serializeBinaryToWriter + ); + } + f = message.getSendToSelf(); + if (f != null) { + writer.writeMessage( + 6, + f, + proto.litrpc.SendToSelf.serializeBinaryToWriter + ); + } + f = message.getChannelRestrict(); + if (f != null) { + writer.writeMessage( + 7, + f, + proto.litrpc.ChannelRestrict.serializeBinaryToWriter + ); + } + f = message.getPeerRestrict(); + if (f != null) { + writer.writeMessage( + 8, + f, + proto.litrpc.PeerRestrict.serializeBinaryToWriter + ); + } +}; + + +/** + * optional RateLimit rate_limit = 1; + * @return {?proto.litrpc.RateLimit} + */ +proto.litrpc.RuleValue.prototype.getRateLimit = function() { + return /** @type{?proto.litrpc.RateLimit} */ ( + jspb.Message.getWrapperField(this, proto.litrpc.RateLimit, 1)); +}; + + +/** @param {?proto.litrpc.RateLimit|undefined} value */ +proto.litrpc.RuleValue.prototype.setRateLimit = function(value) { + jspb.Message.setOneofWrapperField(this, 1, proto.litrpc.RuleValue.oneofGroups_[0], value); +}; + + +proto.litrpc.RuleValue.prototype.clearRateLimit = function() { + this.setRateLimit(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.litrpc.RuleValue.prototype.hasRateLimit = function() { + return jspb.Message.getField(this, 1) != null; +}; + + +/** + * optional ChannelPolicyBounds chan_policy_bounds = 2; + * @return {?proto.litrpc.ChannelPolicyBounds} + */ +proto.litrpc.RuleValue.prototype.getChanPolicyBounds = function() { + return /** @type{?proto.litrpc.ChannelPolicyBounds} */ ( + jspb.Message.getWrapperField(this, proto.litrpc.ChannelPolicyBounds, 2)); +}; + + +/** @param {?proto.litrpc.ChannelPolicyBounds|undefined} value */ +proto.litrpc.RuleValue.prototype.setChanPolicyBounds = function(value) { + jspb.Message.setOneofWrapperField(this, 2, proto.litrpc.RuleValue.oneofGroups_[0], value); +}; + + +proto.litrpc.RuleValue.prototype.clearChanPolicyBounds = function() { + this.setChanPolicyBounds(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.litrpc.RuleValue.prototype.hasChanPolicyBounds = function() { + return jspb.Message.getField(this, 2) != null; +}; + + +/** + * optional HistoryLimit history_limit = 3; + * @return {?proto.litrpc.HistoryLimit} + */ +proto.litrpc.RuleValue.prototype.getHistoryLimit = function() { + return /** @type{?proto.litrpc.HistoryLimit} */ ( + jspb.Message.getWrapperField(this, proto.litrpc.HistoryLimit, 3)); +}; + + +/** @param {?proto.litrpc.HistoryLimit|undefined} value */ +proto.litrpc.RuleValue.prototype.setHistoryLimit = function(value) { + jspb.Message.setOneofWrapperField(this, 3, proto.litrpc.RuleValue.oneofGroups_[0], value); +}; + + +proto.litrpc.RuleValue.prototype.clearHistoryLimit = function() { + this.setHistoryLimit(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.litrpc.RuleValue.prototype.hasHistoryLimit = function() { + return jspb.Message.getField(this, 3) != null; +}; + + +/** + * optional OffChainBudget off_chain_budget = 4; + * @return {?proto.litrpc.OffChainBudget} + */ +proto.litrpc.RuleValue.prototype.getOffChainBudget = function() { + return /** @type{?proto.litrpc.OffChainBudget} */ ( + jspb.Message.getWrapperField(this, proto.litrpc.OffChainBudget, 4)); +}; + + +/** @param {?proto.litrpc.OffChainBudget|undefined} value */ +proto.litrpc.RuleValue.prototype.setOffChainBudget = function(value) { + jspb.Message.setOneofWrapperField(this, 4, proto.litrpc.RuleValue.oneofGroups_[0], value); +}; + + +proto.litrpc.RuleValue.prototype.clearOffChainBudget = function() { + this.setOffChainBudget(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.litrpc.RuleValue.prototype.hasOffChainBudget = function() { + return jspb.Message.getField(this, 4) != null; +}; + + +/** + * optional OnChainBudget on_chain_budget = 5; + * @return {?proto.litrpc.OnChainBudget} + */ +proto.litrpc.RuleValue.prototype.getOnChainBudget = function() { + return /** @type{?proto.litrpc.OnChainBudget} */ ( + jspb.Message.getWrapperField(this, proto.litrpc.OnChainBudget, 5)); +}; + + +/** @param {?proto.litrpc.OnChainBudget|undefined} value */ +proto.litrpc.RuleValue.prototype.setOnChainBudget = function(value) { + jspb.Message.setOneofWrapperField(this, 5, proto.litrpc.RuleValue.oneofGroups_[0], value); +}; + + +proto.litrpc.RuleValue.prototype.clearOnChainBudget = function() { + this.setOnChainBudget(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.litrpc.RuleValue.prototype.hasOnChainBudget = function() { + return jspb.Message.getField(this, 5) != null; +}; + + +/** + * optional SendToSelf send_to_self = 6; + * @return {?proto.litrpc.SendToSelf} + */ +proto.litrpc.RuleValue.prototype.getSendToSelf = function() { + return /** @type{?proto.litrpc.SendToSelf} */ ( + jspb.Message.getWrapperField(this, proto.litrpc.SendToSelf, 6)); +}; + + +/** @param {?proto.litrpc.SendToSelf|undefined} value */ +proto.litrpc.RuleValue.prototype.setSendToSelf = function(value) { + jspb.Message.setOneofWrapperField(this, 6, proto.litrpc.RuleValue.oneofGroups_[0], value); +}; + + +proto.litrpc.RuleValue.prototype.clearSendToSelf = function() { + this.setSendToSelf(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.litrpc.RuleValue.prototype.hasSendToSelf = function() { + return jspb.Message.getField(this, 6) != null; +}; + + +/** + * optional ChannelRestrict channel_restrict = 7; + * @return {?proto.litrpc.ChannelRestrict} + */ +proto.litrpc.RuleValue.prototype.getChannelRestrict = function() { + return /** @type{?proto.litrpc.ChannelRestrict} */ ( + jspb.Message.getWrapperField(this, proto.litrpc.ChannelRestrict, 7)); +}; + + +/** @param {?proto.litrpc.ChannelRestrict|undefined} value */ +proto.litrpc.RuleValue.prototype.setChannelRestrict = function(value) { + jspb.Message.setOneofWrapperField(this, 7, proto.litrpc.RuleValue.oneofGroups_[0], value); +}; + + +proto.litrpc.RuleValue.prototype.clearChannelRestrict = function() { + this.setChannelRestrict(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.litrpc.RuleValue.prototype.hasChannelRestrict = function() { + return jspb.Message.getField(this, 7) != null; +}; + + +/** + * optional PeerRestrict peer_restrict = 8; + * @return {?proto.litrpc.PeerRestrict} + */ +proto.litrpc.RuleValue.prototype.getPeerRestrict = function() { + return /** @type{?proto.litrpc.PeerRestrict} */ ( + jspb.Message.getWrapperField(this, proto.litrpc.PeerRestrict, 8)); +}; + + +/** @param {?proto.litrpc.PeerRestrict|undefined} value */ +proto.litrpc.RuleValue.prototype.setPeerRestrict = function(value) { + jspb.Message.setOneofWrapperField(this, 8, proto.litrpc.RuleValue.oneofGroups_[0], value); +}; + + +proto.litrpc.RuleValue.prototype.clearPeerRestrict = function() { + this.setPeerRestrict(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.litrpc.RuleValue.prototype.hasPeerRestrict = function() { + return jspb.Message.getField(this, 8) != null; +}; + + + +/** + * 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.litrpc.RateLimit = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.litrpc.RateLimit, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.litrpc.RateLimit.displayName = 'proto.litrpc.RateLimit'; +} + + +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.litrpc.RateLimit.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.RateLimit.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.litrpc.RateLimit} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.RateLimit.toObject = function(includeInstance, msg) { + var f, obj = { + readLimit: (f = msg.getReadLimit()) && proto.litrpc.Rate.toObject(includeInstance, f), + writeLimit: (f = msg.getWriteLimit()) && proto.litrpc.Rate.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.litrpc.RateLimit} + */ +proto.litrpc.RateLimit.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.RateLimit; + return proto.litrpc.RateLimit.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.RateLimit} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.RateLimit} + */ +proto.litrpc.RateLimit.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.litrpc.Rate; + reader.readMessage(value,proto.litrpc.Rate.deserializeBinaryFromReader); + msg.setReadLimit(value); + break; + case 2: + var value = new proto.litrpc.Rate; + reader.readMessage(value,proto.litrpc.Rate.deserializeBinaryFromReader); + msg.setWriteLimit(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.RateLimit.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.RateLimit.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.litrpc.RateLimit} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.RateLimit.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getReadLimit(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.litrpc.Rate.serializeBinaryToWriter + ); + } + f = message.getWriteLimit(); + if (f != null) { + writer.writeMessage( + 2, + f, + proto.litrpc.Rate.serializeBinaryToWriter + ); + } +}; + + +/** + * optional Rate read_limit = 1; + * @return {?proto.litrpc.Rate} + */ +proto.litrpc.RateLimit.prototype.getReadLimit = function() { + return /** @type{?proto.litrpc.Rate} */ ( + jspb.Message.getWrapperField(this, proto.litrpc.Rate, 1)); +}; + + +/** @param {?proto.litrpc.Rate|undefined} value */ +proto.litrpc.RateLimit.prototype.setReadLimit = function(value) { + jspb.Message.setWrapperField(this, 1, value); +}; + + +proto.litrpc.RateLimit.prototype.clearReadLimit = function() { + this.setReadLimit(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.litrpc.RateLimit.prototype.hasReadLimit = function() { + return jspb.Message.getField(this, 1) != null; +}; + + +/** + * optional Rate write_limit = 2; + * @return {?proto.litrpc.Rate} + */ +proto.litrpc.RateLimit.prototype.getWriteLimit = function() { + return /** @type{?proto.litrpc.Rate} */ ( + jspb.Message.getWrapperField(this, proto.litrpc.Rate, 2)); +}; + + +/** @param {?proto.litrpc.Rate|undefined} value */ +proto.litrpc.RateLimit.prototype.setWriteLimit = function(value) { + jspb.Message.setWrapperField(this, 2, value); +}; + + +proto.litrpc.RateLimit.prototype.clearWriteLimit = function() { + this.setWriteLimit(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {!boolean} + */ +proto.litrpc.RateLimit.prototype.hasWriteLimit = function() { + return jspb.Message.getField(this, 2) != null; +}; + + + +/** + * 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.litrpc.Rate = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.litrpc.Rate, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.litrpc.Rate.displayName = 'proto.litrpc.Rate'; +} + + +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.litrpc.Rate.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.Rate.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.litrpc.Rate} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.Rate.toObject = function(includeInstance, msg) { + var f, obj = { + iterations: jspb.Message.getFieldWithDefault(msg, 1, 0), + numHours: 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.litrpc.Rate} + */ +proto.litrpc.Rate.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.Rate; + return proto.litrpc.Rate.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.Rate} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.Rate} + */ +proto.litrpc.Rate.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readUint32()); + msg.setIterations(value); + break; + case 2: + var value = /** @type {number} */ (reader.readUint32()); + msg.setNumHours(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.Rate.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.Rate.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.litrpc.Rate} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.Rate.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getIterations(); + if (f !== 0) { + writer.writeUint32( + 1, + f + ); + } + f = message.getNumHours(); + if (f !== 0) { + writer.writeUint32( + 2, + f + ); + } +}; + + +/** + * optional uint32 iterations = 1; + * @return {number} + */ +proto.litrpc.Rate.prototype.getIterations = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** @param {number} value */ +proto.litrpc.Rate.prototype.setIterations = function(value) { + jspb.Message.setProto3IntField(this, 1, value); +}; + + +/** + * optional uint32 num_hours = 2; + * @return {number} + */ +proto.litrpc.Rate.prototype.getNumHours = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** @param {number} value */ +proto.litrpc.Rate.prototype.setNumHours = function(value) { + jspb.Message.setProto3IntField(this, 2, 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.litrpc.HistoryLimit = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.litrpc.HistoryLimit, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.litrpc.HistoryLimit.displayName = 'proto.litrpc.HistoryLimit'; +} + + +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.litrpc.HistoryLimit.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.HistoryLimit.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.litrpc.HistoryLimit} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.HistoryLimit.toObject = function(includeInstance, msg) { + var f, obj = { + startTime: jspb.Message.getFieldWithDefault(msg, 1, "0"), + duration: 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.litrpc.HistoryLimit} + */ +proto.litrpc.HistoryLimit.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.HistoryLimit; + return proto.litrpc.HistoryLimit.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.HistoryLimit} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.HistoryLimit} + */ +proto.litrpc.HistoryLimit.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setStartTime(value); + break; + case 2: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setDuration(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.HistoryLimit.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.HistoryLimit.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.litrpc.HistoryLimit} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.HistoryLimit.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getStartTime(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 1, + f + ); + } + f = message.getDuration(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 2, + f + ); + } +}; + + +/** + * optional uint64 start_time = 1; + * @return {string} + */ +proto.litrpc.HistoryLimit.prototype.getStartTime = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "0")); +}; + + +/** @param {string} value */ +proto.litrpc.HistoryLimit.prototype.setStartTime = function(value) { + jspb.Message.setProto3StringIntField(this, 1, value); +}; + + +/** + * optional uint64 duration = 2; + * @return {string} + */ +proto.litrpc.HistoryLimit.prototype.getDuration = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0")); +}; + + +/** @param {string} value */ +proto.litrpc.HistoryLimit.prototype.setDuration = function(value) { + jspb.Message.setProto3StringIntField(this, 2, 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.litrpc.ChannelPolicyBounds = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.litrpc.ChannelPolicyBounds, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.litrpc.ChannelPolicyBounds.displayName = 'proto.litrpc.ChannelPolicyBounds'; +} + + +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.litrpc.ChannelPolicyBounds.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.ChannelPolicyBounds.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.litrpc.ChannelPolicyBounds} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.ChannelPolicyBounds.toObject = function(includeInstance, msg) { + var f, obj = { + minBaseMsat: jspb.Message.getFieldWithDefault(msg, 1, "0"), + maxBaseMsat: jspb.Message.getFieldWithDefault(msg, 2, "0"), + minRatePpm: jspb.Message.getFieldWithDefault(msg, 3, 0), + maxRatePpm: jspb.Message.getFieldWithDefault(msg, 4, 0), + minCltvDelta: jspb.Message.getFieldWithDefault(msg, 5, 0), + maxCltvDelta: jspb.Message.getFieldWithDefault(msg, 6, 0), + minHtlcMsat: jspb.Message.getFieldWithDefault(msg, 7, "0"), + maxHtlcMsat: jspb.Message.getFieldWithDefault(msg, 8, "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.litrpc.ChannelPolicyBounds} + */ +proto.litrpc.ChannelPolicyBounds.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.ChannelPolicyBounds; + return proto.litrpc.ChannelPolicyBounds.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.ChannelPolicyBounds} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.ChannelPolicyBounds} + */ +proto.litrpc.ChannelPolicyBounds.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setMinBaseMsat(value); + break; + case 2: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setMaxBaseMsat(value); + break; + case 3: + var value = /** @type {number} */ (reader.readUint32()); + msg.setMinRatePpm(value); + break; + case 4: + var value = /** @type {number} */ (reader.readUint32()); + msg.setMaxRatePpm(value); + break; + case 5: + var value = /** @type {number} */ (reader.readUint32()); + msg.setMinCltvDelta(value); + break; + case 6: + var value = /** @type {number} */ (reader.readUint32()); + msg.setMaxCltvDelta(value); + break; + case 7: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setMinHtlcMsat(value); + break; + case 8: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setMaxHtlcMsat(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.ChannelPolicyBounds.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.ChannelPolicyBounds.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.litrpc.ChannelPolicyBounds} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.ChannelPolicyBounds.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getMinBaseMsat(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 1, + f + ); + } + f = message.getMaxBaseMsat(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 2, + f + ); + } + f = message.getMinRatePpm(); + if (f !== 0) { + writer.writeUint32( + 3, + f + ); + } + f = message.getMaxRatePpm(); + if (f !== 0) { + writer.writeUint32( + 4, + f + ); + } + f = message.getMinCltvDelta(); + if (f !== 0) { + writer.writeUint32( + 5, + f + ); + } + f = message.getMaxCltvDelta(); + if (f !== 0) { + writer.writeUint32( + 6, + f + ); + } + f = message.getMinHtlcMsat(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 7, + f + ); + } + f = message.getMaxHtlcMsat(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 8, + f + ); + } +}; + + +/** + * optional uint64 min_base_msat = 1; + * @return {string} + */ +proto.litrpc.ChannelPolicyBounds.prototype.getMinBaseMsat = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "0")); +}; + + +/** @param {string} value */ +proto.litrpc.ChannelPolicyBounds.prototype.setMinBaseMsat = function(value) { + jspb.Message.setProto3StringIntField(this, 1, value); +}; + + +/** + * optional uint64 max_base_msat = 2; + * @return {string} + */ +proto.litrpc.ChannelPolicyBounds.prototype.getMaxBaseMsat = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0")); +}; + + +/** @param {string} value */ +proto.litrpc.ChannelPolicyBounds.prototype.setMaxBaseMsat = function(value) { + jspb.Message.setProto3StringIntField(this, 2, value); +}; + + +/** + * optional uint32 min_rate_ppm = 3; + * @return {number} + */ +proto.litrpc.ChannelPolicyBounds.prototype.getMinRatePpm = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** @param {number} value */ +proto.litrpc.ChannelPolicyBounds.prototype.setMinRatePpm = function(value) { + jspb.Message.setProto3IntField(this, 3, value); +}; + + +/** + * optional uint32 max_rate_ppm = 4; + * @return {number} + */ +proto.litrpc.ChannelPolicyBounds.prototype.getMaxRatePpm = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 4, 0)); +}; + + +/** @param {number} value */ +proto.litrpc.ChannelPolicyBounds.prototype.setMaxRatePpm = function(value) { + jspb.Message.setProto3IntField(this, 4, value); +}; + + +/** + * optional uint32 min_cltv_delta = 5; + * @return {number} + */ +proto.litrpc.ChannelPolicyBounds.prototype.getMinCltvDelta = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 5, 0)); +}; + + +/** @param {number} value */ +proto.litrpc.ChannelPolicyBounds.prototype.setMinCltvDelta = function(value) { + jspb.Message.setProto3IntField(this, 5, value); +}; + + +/** + * optional uint32 max_cltv_delta = 6; + * @return {number} + */ +proto.litrpc.ChannelPolicyBounds.prototype.getMaxCltvDelta = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 6, 0)); +}; + + +/** @param {number} value */ +proto.litrpc.ChannelPolicyBounds.prototype.setMaxCltvDelta = function(value) { + jspb.Message.setProto3IntField(this, 6, value); +}; + + +/** + * optional uint64 min_htlc_msat = 7; + * @return {string} + */ +proto.litrpc.ChannelPolicyBounds.prototype.getMinHtlcMsat = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 7, "0")); +}; + + +/** @param {string} value */ +proto.litrpc.ChannelPolicyBounds.prototype.setMinHtlcMsat = function(value) { + jspb.Message.setProto3StringIntField(this, 7, value); +}; + + +/** + * optional uint64 max_htlc_msat = 8; + * @return {string} + */ +proto.litrpc.ChannelPolicyBounds.prototype.getMaxHtlcMsat = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 8, "0")); +}; + + +/** @param {string} value */ +proto.litrpc.ChannelPolicyBounds.prototype.setMaxHtlcMsat = function(value) { + jspb.Message.setProto3StringIntField(this, 8, 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.litrpc.OffChainBudget = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.litrpc.OffChainBudget, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.litrpc.OffChainBudget.displayName = 'proto.litrpc.OffChainBudget'; +} + + +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.litrpc.OffChainBudget.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.OffChainBudget.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.litrpc.OffChainBudget} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.OffChainBudget.toObject = function(includeInstance, msg) { + var f, obj = { + maxAmtMsat: jspb.Message.getFieldWithDefault(msg, 1, "0"), + maxFeesMsat: 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.litrpc.OffChainBudget} + */ +proto.litrpc.OffChainBudget.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.OffChainBudget; + return proto.litrpc.OffChainBudget.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.OffChainBudget} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.OffChainBudget} + */ +proto.litrpc.OffChainBudget.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setMaxAmtMsat(value); + break; + case 2: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setMaxFeesMsat(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.OffChainBudget.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.OffChainBudget.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.litrpc.OffChainBudget} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.OffChainBudget.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getMaxAmtMsat(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 1, + f + ); + } + f = message.getMaxFeesMsat(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 2, + f + ); + } +}; + + +/** + * optional uint64 max_amt_msat = 1; + * @return {string} + */ +proto.litrpc.OffChainBudget.prototype.getMaxAmtMsat = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "0")); +}; + + +/** @param {string} value */ +proto.litrpc.OffChainBudget.prototype.setMaxAmtMsat = function(value) { + jspb.Message.setProto3StringIntField(this, 1, value); +}; + + +/** + * optional uint64 max_fees_msat = 2; + * @return {string} + */ +proto.litrpc.OffChainBudget.prototype.getMaxFeesMsat = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0")); +}; + + +/** @param {string} value */ +proto.litrpc.OffChainBudget.prototype.setMaxFeesMsat = function(value) { + jspb.Message.setProto3StringIntField(this, 2, 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.litrpc.OnChainBudget = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.litrpc.OnChainBudget, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.litrpc.OnChainBudget.displayName = 'proto.litrpc.OnChainBudget'; +} + + +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.litrpc.OnChainBudget.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.OnChainBudget.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.litrpc.OnChainBudget} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.OnChainBudget.toObject = function(includeInstance, msg) { + var f, obj = { + absoluteAmtSats: jspb.Message.getFieldWithDefault(msg, 1, "0"), + maxSatPerVByte: 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.litrpc.OnChainBudget} + */ +proto.litrpc.OnChainBudget.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.OnChainBudget; + return proto.litrpc.OnChainBudget.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.OnChainBudget} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.OnChainBudget} + */ +proto.litrpc.OnChainBudget.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setAbsoluteAmtSats(value); + break; + case 2: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setMaxSatPerVByte(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.OnChainBudget.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.OnChainBudget.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.litrpc.OnChainBudget} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.OnChainBudget.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getAbsoluteAmtSats(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 1, + f + ); + } + f = message.getMaxSatPerVByte(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 2, + f + ); + } +}; + + +/** + * optional uint64 absolute_amt_sats = 1; + * @return {string} + */ +proto.litrpc.OnChainBudget.prototype.getAbsoluteAmtSats = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "0")); +}; + + +/** @param {string} value */ +proto.litrpc.OnChainBudget.prototype.setAbsoluteAmtSats = function(value) { + jspb.Message.setProto3StringIntField(this, 1, value); +}; + + +/** + * optional uint64 max_sat_per_v_byte = 2; + * @return {string} + */ +proto.litrpc.OnChainBudget.prototype.getMaxSatPerVByte = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0")); +}; + + +/** @param {string} value */ +proto.litrpc.OnChainBudget.prototype.setMaxSatPerVByte = function(value) { + jspb.Message.setProto3StringIntField(this, 2, 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.litrpc.SendToSelf = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.litrpc.SendToSelf, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.litrpc.SendToSelf.displayName = 'proto.litrpc.SendToSelf'; +} + + +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.litrpc.SendToSelf.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.SendToSelf.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.litrpc.SendToSelf} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.SendToSelf.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.litrpc.SendToSelf} + */ +proto.litrpc.SendToSelf.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.SendToSelf; + return proto.litrpc.SendToSelf.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.SendToSelf} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.SendToSelf} + */ +proto.litrpc.SendToSelf.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.SendToSelf.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.SendToSelf.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.litrpc.SendToSelf} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.SendToSelf.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + +/** + * 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.litrpc.ChannelRestrict = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.litrpc.ChannelRestrict.repeatedFields_, null); +}; +goog.inherits(proto.litrpc.ChannelRestrict, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.litrpc.ChannelRestrict.displayName = 'proto.litrpc.ChannelRestrict'; +} +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.litrpc.ChannelRestrict.repeatedFields_ = [1]; + + + +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.litrpc.ChannelRestrict.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.ChannelRestrict.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.litrpc.ChannelRestrict} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.ChannelRestrict.toObject = function(includeInstance, msg) { + var f, obj = { + channelIdsList: jspb.Message.getRepeatedField(msg, 1) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.litrpc.ChannelRestrict} + */ +proto.litrpc.ChannelRestrict.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.ChannelRestrict; + return proto.litrpc.ChannelRestrict.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.ChannelRestrict} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.ChannelRestrict} + */ +proto.litrpc.ChannelRestrict.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {!Array} */ (reader.readPackedUint64String()); + msg.setChannelIdsList(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.ChannelRestrict.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.ChannelRestrict.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.litrpc.ChannelRestrict} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.ChannelRestrict.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getChannelIdsList(); + if (f.length > 0) { + writer.writePackedUint64String( + 1, + f + ); + } +}; + + +/** + * repeated uint64 channel_ids = 1; + * @return {!Array} + */ +proto.litrpc.ChannelRestrict.prototype.getChannelIdsList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 1)); +}; + + +/** @param {!Array} value */ +proto.litrpc.ChannelRestrict.prototype.setChannelIdsList = function(value) { + jspb.Message.setField(this, 1, value || []); +}; + + +/** + * @param {!string} value + * @param {number=} opt_index + */ +proto.litrpc.ChannelRestrict.prototype.addChannelIds = function(value, opt_index) { + jspb.Message.addToRepeatedField(this, 1, value, opt_index); +}; + + +proto.litrpc.ChannelRestrict.prototype.clearChannelIdsList = function() { + this.setChannelIdsList([]); +}; + + + +/** + * 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.litrpc.PeerRestrict = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.litrpc.PeerRestrict.repeatedFields_, null); +}; +goog.inherits(proto.litrpc.PeerRestrict, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.litrpc.PeerRestrict.displayName = 'proto.litrpc.PeerRestrict'; +} +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.litrpc.PeerRestrict.repeatedFields_ = [1]; + + + +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.litrpc.PeerRestrict.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.PeerRestrict.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.litrpc.PeerRestrict} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.PeerRestrict.toObject = function(includeInstance, msg) { + var f, obj = { + peerIdsList: jspb.Message.getRepeatedField(msg, 1) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.litrpc.PeerRestrict} + */ +proto.litrpc.PeerRestrict.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.PeerRestrict; + return proto.litrpc.PeerRestrict.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.PeerRestrict} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.PeerRestrict} + */ +proto.litrpc.PeerRestrict.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.addPeerIds(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.PeerRestrict.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.PeerRestrict.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.litrpc.PeerRestrict} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.PeerRestrict.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getPeerIdsList(); + if (f.length > 0) { + writer.writeRepeatedString( + 1, + f + ); + } +}; + + +/** + * repeated string peer_ids = 1; + * @return {!Array} + */ +proto.litrpc.PeerRestrict.prototype.getPeerIdsList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 1)); +}; + + +/** @param {!Array} value */ +proto.litrpc.PeerRestrict.prototype.setPeerIdsList = function(value) { + jspb.Message.setField(this, 1, value || []); +}; + + +/** + * @param {!string} value + * @param {number=} opt_index + */ +proto.litrpc.PeerRestrict.prototype.addPeerIds = function(value, opt_index) { + jspb.Message.addToRepeatedField(this, 1, value, opt_index); +}; + + +proto.litrpc.PeerRestrict.prototype.clearPeerIdsList = function() { + this.setPeerIdsList([]); +}; + + /** * @enum {number} */ @@ -2025,6 +4438,7 @@ proto.litrpc.SessionType = { TYPE_MACAROON_ADMIN: 1, TYPE_MACAROON_CUSTOM: 2, TYPE_UI_PASSWORD: 3, + TYPE_AUTOPILOT: 4, TYPE_MACAROON_ACCOUNT: 5 }; diff --git a/app/src/util/tests/sampleData.ts b/app/src/util/tests/sampleData.ts index 7f2f80179..8a352d712 100644 --- a/app/src/util/tests/sampleData.ts +++ b/app/src/util/tests/sampleData.ts @@ -887,6 +887,7 @@ export const poolRegisterSidecar: POOL.SidecarTicket.AsObject = { export const litListSessions: LIT.ListSessionsResponse.AsObject = { sessionsList: [ { + id: '', devServer: true, expiryTimestampSeconds: '253370782800', label: 'Default Session', @@ -899,8 +900,74 @@ export const litListSessions: LIT.ListSessionsResponse.AsObject = { sessionType: LIT.SessionType.TYPE_UI_PASSWORD, createdAt: '253300000000', accountId: '', + revokedAt: '453300000000', + autopilotFeatureInfoMap: [ + [ + 'SampleFeature', + { + rulesMap: [ + [ + 'channel-policy-bounds', + { + chanPolicyBounds: { + minBaseMsat: '0', + maxBaseMsat: '10', + minRatePpm: 1, + maxRatePpm: 10, + minCltvDelta: 18, + maxCltvDelta: 18, + minHtlcMsat: '0', + maxHtlcMsat: '0', + }, + }, + ], + [ + 'channel-restriction', + { + channelRestrict: { + channelIdsList: [], + }, + }, + ], + [ + 'history-limit', + { + historyLimit: { + startTime: '0', + duration: '0', + }, + }, + ], + [ + 'peer-restriction', + { + peerRestrict: { + peerIdsList: [], + }, + }, + ], + [ + 'rate-limit', + { + rateLimit: { + readLimit: { + iterations: 50, + numHours: 1, + }, + writeLimit: { + iterations: 50, + numHours: 50, + }, + }, + }, + ], + ], + }, + ], + ], }, { + id: '', devServer: true, expiryTimestampSeconds: '253370782800', label: 'Default Session', @@ -912,7 +979,72 @@ export const litListSessions: LIT.ListSessionsResponse.AsObject = { sessionState: LIT.SessionState.STATE_EXPIRED, sessionType: LIT.SessionType.TYPE_UI_PASSWORD, createdAt: '253300000000', + revokedAt: '453300000000', accountId: '', + autopilotFeatureInfoMap: [ + [ + 'SampleFeature', + { + rulesMap: [ + [ + 'channel-policy-bounds', + { + chanPolicyBounds: { + minBaseMsat: '0', + maxBaseMsat: '10', + minRatePpm: 1, + maxRatePpm: 10, + minCltvDelta: 18, + maxCltvDelta: 18, + minHtlcMsat: '0', + maxHtlcMsat: '0', + }, + }, + ], + [ + 'channel-restriction', + { + channelRestrict: { + channelIdsList: [], + }, + }, + ], + [ + 'history-limit', + { + historyLimit: { + startTime: '0', + duration: '0', + }, + }, + ], + [ + 'peer-restriction', + { + peerRestrict: { + peerIdsList: [], + }, + }, + ], + [ + 'rate-limit', + { + rateLimit: { + readLimit: { + iterations: 50, + numHours: 1, + }, + writeLimit: { + iterations: 50, + numHours: 50, + }, + }, + }, + ], + ], + }, + ], + ], }, ], }; diff --git a/autopilotserver/client.go b/autopilotserver/client.go new file mode 100644 index 000000000..599c0466a --- /dev/null +++ b/autopilotserver/client.go @@ -0,0 +1,640 @@ +package autopilotserver + +import ( + "context" + "crypto/tls" + "encoding/hex" + "errors" + "fmt" + "net" + "strings" + "sync" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/lightning-terminal/autopilotserverrpc" + "github.com/lightningnetwork/lnd/tor" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +// ErrVersionIncompatible is returned when the minimum Lit version required by +// the autopilot server exceeds the version of this binary. +var ErrVersionIncompatible = fmt.Errorf("litd version is not compatible " + + "with the minimum version required by the autopilot server") + +// Config holds the configuration options for the autopilot server client. +type Config struct { + // Disable will disable the autopilot client. + Disable bool `long:"disable" description:"disable the autopilot client"` + + // Address is the domain:port of the autopilot server. + Address string `long:"address" description:"autopilot server address host:port"` + + // Proxy is the SOCKS proxy that should be used to establish the + // connection. + Proxy string `long:"proxy" description:"The host:port of a SOCKS proxy through which all connections to the autopilot server will be established over"` + + // Insecure signals that no TLS should be used if set to true. + Insecure bool `long:"insecure" description:"disable tls"` + + // TLSPath is the path to a local file that holds the autopilot + // server's TLS certificate. This is only needed if the server is using + // a self-signed cert. + TLSPath string `long:"tlspath" description:"Path to autopilot server tls certificate"` + + // PingCadence determines how often the Autopilot client should + // re-register an existing session with the Autopilot server to ensure + // that the Autopilot server knows that the session is active. + PingCadence time.Duration `long:"pingcadence" description:"How often the client should ensure that registered Autopilot sessions are active"` + + // DialOpts is a list of additional options that should be used when + // dialing the gRPC connection. + DialOpts []grpc.DialOption + + // LitVersion is the version of the Lit binary. + LitVersion Version + + // LndVersion is the version of the connected LND binary. + LndVersion Version +} + +// Version defines a software version. +type Version struct { + // Major is the major version of the software. + Major uint32 + + // Minor is the major version of the software. + Minor uint32 + + // Patch is the patch number that the software is running. + Patch uint32 +} + +// A compile-time check to ensure that the Client struct implements the +// Autopilot interface. +var _ Autopilot = (*Client)(nil) + +// Client is a client connection to the autopilot server. +type Client struct { + start sync.Once + stop sync.Once + + cfg *Config + + sessions map[string]*session + sessionsMu sync.Mutex + + featurePerms *featurePerms + + quit chan struct{} + wg sync.WaitGroup +} + +type session struct { + key *btcec.PublicKey + lastSuccess time.Time +} + +type featurePerms struct { + perms map[string]map[string]bool + lastUpdated time.Time + sync.Mutex +} + +// NewClient returns a autopilot-server client. +func NewClient(cfg *Config) (Autopilot, error) { + var err error + cfg.DialOpts, err = getAutopilotServerDialOpts( + cfg.Insecure, cfg.Proxy, cfg.TLSPath, cfg.DialOpts..., + ) + if err != nil { + return nil, err + } + + return &Client{ + cfg: cfg, + sessions: make(map[string]*session), + quit: make(chan struct{}), + featurePerms: &featurePerms{ + perms: make(map[string]map[string]bool), + }, + }, nil +} + +// Start kicks off all the goroutines required by the Client. +func (c *Client) Start(opts ...func(cfg *Config)) error { + var startErr error + c.start.Do(func() { + log.Infof("Starting Autopilot Client") + + for _, o := range opts { + o(c.cfg) + } + + version, err := c.getMinVersion(context.Background()) + if err != nil { + startErr = err + return + } + + err = c.checkCompatibility(version) + if err != nil { + startErr = err + if !errors.Is(err, ErrVersionIncompatible) { + return + } + + startErr = fmt.Errorf("lit must be on at least "+ + "version v%d.%d.%d to be compatile with the "+ + "autopilot server", version.Major, + version.Minor, version.Patch) + return + } + + c.wg.Add(2) + go c.activateSessionsForever() + go c.updateFeaturePermsForever() + }) + + return startErr +} + +// Stop cleans up any resources or goroutines managed by the Client. +func (c *Client) Stop() { + c.stop.Do(func() { + close(c.quit) + c.wg.Wait() + }) +} + +// ListFeaturePerms returns contents of the in-memory feature permissions list +// if it has been populated. +func (c *Client) ListFeaturePerms(_ context.Context) ( + map[string]map[string]bool, error) { + + c.featurePerms.Lock() + defer c.featurePerms.Unlock() + + if c.featurePerms.lastUpdated.IsZero() { + return nil, fmt.Errorf("feature permissions list is not yet " + + "populated") + } + + return c.featurePerms.perms, nil +} + +// SessionRevoked removes a session from the list of active sessions managed by +// the client. +// +// Note: this is part of the Autopilot interface. +func (c *Client) SessionRevoked(ctx context.Context, pubKey *btcec.PublicKey) { + key := hex.EncodeToString(pubKey.SerializeCompressed()) + + c.sessionsMu.Lock() + delete(c.sessions, key) + c.sessionsMu.Unlock() + + // Do a best-effort call to the Autopilot server to notify it that the + // session is being revoked. It is not the end of the world if this call + // fails since the Autopilot will move the session to inactive itself + // after a few unsuccessful connection attempts. + client, cleanup, err := c.getClientConn() + if err != nil { + log.Errorf("could not get client connection: %v", err) + return + } + defer cleanup() + + _, err = client.RevokeSession( + ctx, &autopilotserverrpc.RevokeSessionRequest{ + ResponderPubKey: pubKey.SerializeCompressed(), + }, + ) + if err != nil { + log.Errorf("could not revoke session %x: %v", + pubKey.SerializeCompressed(), err) + + return + } +} + +// activateSessionsForever periodically ensures that each of our active +// autopilot sessions are known by the autopilot to be active. +func (c *Client) activateSessionsForever() { + defer c.wg.Done() + + ctx := context.Background() + ticker := time.NewTicker(c.cfg.PingCadence) + defer ticker.Stop() + + for { + c.sessionsMu.Lock() + for _, s := range c.sessions { + // If the session was recently registered with the + // autopilot server, then we don't need to register it + // again so soon. + if !s.lastSuccess.IsZero() && + time.Since(s.lastSuccess) < c.cfg.PingCadence { + + continue + } + + key := hex.EncodeToString(s.key.SerializeCompressed()) + + log.Debugf("ensuring activation of session %s", key) + + perm, err := c.activateSession(ctx, s.key) + if err != nil { + log.Errorf("could not activate session %s: %v", + key, err) + + if perm { + delete(c.sessions, key) + } + + continue + } + + s.lastSuccess = time.Now() + } + c.sessionsMu.Unlock() + + select { + case <-ticker.C: + case <-c.quit: + return + } + } +} + +// updateFeaturePermsForever periodically attempts to update the in-memory +// feature permissions list. +// +// NOTE: this MUST be called in a goroutine. +func (c *Client) updateFeaturePermsForever() { + defer c.wg.Done() + + ctx := context.Background() + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + const ( + retryTime = time.Minute + refreshTime = time.Hour * 24 + ) + + for { + select { + case <-ticker.C: + case <-c.quit: + return + } + + c.featurePerms.Lock() + sinceUpdate := time.Since(c.featurePerms.lastUpdated) + c.featurePerms.Unlock() + + if sinceUpdate < refreshTime { + ticker.Reset(refreshTime - sinceUpdate) + continue + } + + features, err := c.ListFeatures(ctx) + if err != nil { + log.Errorf("could not fetch features from "+ + "autopilot server: %v", err) + + ticker.Reset(retryTime) + continue + } + + c.updateFeaturePerms(features) + ticker.Reset(refreshTime) + } +} + +// updateFeaturePerms updates our in-memory store of the permissions required +// for each feature. +func (c *Client) updateFeaturePerms(features map[string]*Feature) { + perms := make(map[string]map[string]bool) + for name, feature := range features { + perms[name] = make(map[string]bool) + for p := range feature.Permissions { + perms[name][p] = true + } + } + + c.featurePerms.Lock() + c.featurePerms.perms = perms + c.featurePerms.lastUpdated = time.Now() + c.featurePerms.Unlock() +} + +// ListFeatures queries the autopilot server for all the features it has +// available. +// +// Note: this is part of the Autopilot interface. +func (c *Client) ListFeatures(ctx context.Context) (map[string]*Feature, + error) { + + client, cleanup, err := c.getClientConn() + if err != nil { + return nil, err + } + defer cleanup() + + resp, err := client.ListFeatures( + ctx, &autopilotserverrpc.ListFeaturesRequest{}, + ) + if err != nil { + return nil, err + } + + features := make(map[string]*Feature, len(resp.Features)) + for i, feature := range resp.Features { + perms := unmarshalPermissions(feature.PermissionsList) + rules := unmarshalRules(feature.Rules) + features[i] = &Feature{ + Name: feature.Name, + Description: feature.Description, + Permissions: perms, + Rules: rules, + } + } + + // Take this opportunity to also update the feature permissions map. + c.updateFeaturePerms(features) + + return features, nil +} + +// RegisterSession attempts to register a session with the autopilot server. +// If the registration is successful, then the Client will also track the +// session so that it can continuously ensure that the session remains active. +// +// Note: this is part of the Autopilot interface. +func (c *Client) RegisterSession(ctx context.Context, pubKey *btcec.PublicKey, + mailboxAddr string, devServer bool, featureConf map[string][]byte) ( + *btcec.PublicKey, error) { + + remotePub, err := c.registerSession( + ctx, pubKey, mailboxAddr, devServer, featureConf, + ) + if err != nil { + log.Errorf("unsuccessful registration of session %x", + pubKey.SerializeCompressed()) + + return nil, err + } + + c.trackClient(pubKey) + + return remotePub, nil +} + +// ActivateSession attempts to inform the autopilot server that the given +// session is still active. It also adds the session to the list tracked by +// the client so that the Client can ensure that the session remains active on +// the autopilot side. +// +// Note: this is part of the Autopilot interface. +func (c *Client) ActivateSession(ctx context.Context, pubKey *btcec.PublicKey) ( + bool, error) { + + c.trackClient(pubKey) + + return c.activateSession(ctx, pubKey) +} + +// trackClient adds the given session key to the set of sessions tracked by the +// Client. +func (c *Client) trackClient(pubKey *btcec.PublicKey) { + c.sessionsMu.Lock() + defer c.sessionsMu.Unlock() + + key := hex.EncodeToString(pubKey.SerializeCompressed()) + + c.sessions[key] = &session{ + key: pubKey, + lastSuccess: time.Now(), + } +} + +// registerSession attempts to register a session with the given local static +// public key with the autopilot server. +func (c *Client) registerSession(ctx context.Context, pubKey *btcec.PublicKey, + mailboxAddr string, devServer bool, + featureConfig map[string][]byte) (*btcec.PublicKey, error) { + + client, cleanup, err := c.getClientConn() + if err != nil { + return nil, err + } + defer cleanup() + + resp, err := client.RegisterSession( + ctx, &autopilotserverrpc.RegisterSessionRequest{ + ResponderPubKey: pubKey.SerializeCompressed(), + MailboxAddr: mailboxAddr, + DevServer: devServer, + FeatureConfigs: featureConfig, + LitVersion: marshalVersion(c.cfg.LitVersion), + LndVersion: marshalVersion(c.cfg.LndVersion), + }, + ) + if err != nil { + return nil, err + } + + return btcec.ParsePubKey(resp.InitiatorPubKey) +} + +func (c *Client) getMinVersion(ctx context.Context) (*Version, error) { + client, cleanup, err := c.getClientConn() + if err != nil { + return nil, err + } + defer cleanup() + + terms, err := client.Terms(ctx, &autopilotserverrpc.TermsRequest{}) + if err != nil { + return nil, err + } + + return &Version{ + Major: terms.MinRequiredVersion.Major, + Minor: terms.MinRequiredVersion.Minor, + Patch: terms.MinRequiredVersion.Patch, + }, nil +} + +func (c *Client) checkCompatibility(minV *Version) error { + v := c.cfg.LitVersion + + if v.Major != minV.Major { + if v.Major > minV.Major { + return nil + } + return ErrVersionIncompatible + } + + if v.Minor != minV.Minor { + if v.Minor > minV.Minor { + return nil + } + return ErrVersionIncompatible + } + + if v.Patch != minV.Patch { + if v.Patch > minV.Patch { + return nil + } + return ErrVersionIncompatible + } + + // The actual version and expected version are identical. + return nil +} + +// activateSession ensures that the autopilot server sees the given session as +// active. The returned boolean is true if the error is permanent and the +// session should be revoked. If it is false, then the client should retry +// again in the future. +func (c *Client) activateSession(ctx context.Context, pubKey *btcec.PublicKey) ( + bool, error) { + + client, cleanup, err := c.getClientConn() + if err != nil { + return false, err + } + defer cleanup() + + _, err = client.ActivateSession( + ctx, &autopilotserverrpc.ActivateSessionRequest{ + ResponderPubKey: pubKey.SerializeCompressed(), + }, + ) + if err == nil { + return false, nil + } + + // TODO(elle): use structured GRPC errors instead. + if strings.Contains(err.Error(), "the client has been rejected") { + return true, err + } + + return false, err +} + +// getClientConn creates a connection to the autopilot server and returns this +// connection along with a cleanup function to be used when the connection +// is no longer needed. +func (c *Client) getClientConn() (autopilotserverrpc.AutopilotClient, + func(), error) { + + serverConn, err := grpc.Dial(c.cfg.Address, c.cfg.DialOpts...) + if err != nil { + return nil, nil, fmt.Errorf("unable to connect to RPC "+ + "server: %v", err) + } + + clientConn := autopilotserverrpc.NewAutopilotClient(serverConn) + + return clientConn, func() { + err = serverConn.Close() + if err != nil { + log.Errorf("could not close server conn: %v", err) + } + }, nil +} + +// getAutopilotServerDialOpts returns the dial options to connect to the +// autopilot server. +func getAutopilotServerDialOpts(insecure bool, proxyAddress, tlsPath string, + dialOpts ...grpc.DialOption) ([]grpc.DialOption, error) { + + // Create a copy of the dial options array. + opts := dialOpts + + // There are three options to connect to an autopilot server, either + // insecure, using a self-signed certificate or with a certificate + // signed by a public CA. + switch { + case insecure: + opts = append(opts, grpc.WithInsecure()) + + case tlsPath != "": + // Load the specified TLS certificate and build + // transport credentials + creds, err := credentials.NewClientTLSFromFile(tlsPath, "") + if err != nil { + return nil, err + } + opts = append(opts, grpc.WithTransportCredentials(creds)) + + default: + creds := credentials.NewTLS(&tls.Config{}) + opts = append(opts, grpc.WithTransportCredentials(creds)) + } + // If a SOCKS proxy address was specified, + // then we should dial through it. + if proxyAddress != "" { + log.Infof("Proxying connection to autopilotserver server "+ + "over Tor SOCKS proxy %v", proxyAddress) + torDialer := func(_ context.Context, addr string) (net.Conn, + error) { + + return tor.Dial( + addr, proxyAddress, false, false, + tor.DefaultConnTimeout, + ) + } + opts = append(opts, grpc.WithContextDialer(torDialer)) + } + + return opts, nil +} + +func unmarshalPermissions( + perms []*autopilotserverrpc.Permissions) map[string][]bakery.Op { + + res := make(map[string][]bakery.Op) + for _, perm := range perms { + operations := make([]bakery.Op, len(perm.Operations)) + for i, op := range perm.Operations { + operations[i] = bakery.Op{ + Entity: op.Entity, + Action: op.Action, + } + } + + res[perm.Method] = operations + } + + return res +} + +func unmarshalRules( + rules map[string]*autopilotserverrpc.Rule) map[string]*RuleValues { + + res := make(map[string]*RuleValues, len(rules)) + for name, rule := range rules { + res[name] = &RuleValues{ + Default: rule.Default, + MinVal: rule.MinValue, + MaxVal: rule.MaxValue, + } + } + + return res +} + +func marshalVersion(v Version) *autopilotserverrpc.Version { + return &autopilotserverrpc.Version{ + Major: v.Major, + Minor: v.Minor, + Patch: v.Patch, + } +} diff --git a/autopilotserver/client_test.go b/autopilotserver/client_test.go new file mode 100644 index 000000000..755b3a640 --- /dev/null +++ b/autopilotserver/client_test.go @@ -0,0 +1,87 @@ +package autopilotserver + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/lightning-terminal/autopilotserver/mock" + "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/stretchr/testify/require" +) + +// TestAutopilotClient tests that the Client correctly interacts with the +// Autopilot server. +func TestAutopilotClient(t *testing.T) { + ctx := context.Background() + + // Spin up a new mock Autopilot server. + server := mock.NewServer() + require.NoError(t, server.Start()) + t.Cleanup(server.Stop) + + // Create a new client and connect it to the mock server. We set a very + // short ping cadence so that we can test that the client correctly + // ensures re-activation of a session. + addr := fmt.Sprintf("localhost:%d", server.GetPort()) + client, err := NewClient(&Config{ + Address: addr, + Insecure: true, + PingCadence: time.Second, + }) + require.NoError(t, err) + require.NoError(t, client.Start()) + t.Cleanup(client.Stop) + + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + pubKey := privKey.PubKey() + + // Activating a session that the server does not yet know about should + // error. + _, err = client.ActivateSession(ctx, pubKey) + require.ErrorContains(t, err, "no such client") + + // Register the client. + _, err = client.RegisterSession(ctx, pubKey, "", false, nil) + require.NoError(t, err) + + // Assert that the server sees the new client and has it in the Active + // state. + state, err := server.GetClientState(pubKey) + require.NoError(t, err) + require.True(t, mock.ClientStateActive == state) + + // Let the server move the client to an Inactive state. + err = server.SetClientState(pubKey, mock.ClientStateInactive) + require.NoError(t, err) + state, err = server.GetClientState(pubKey) + require.NoError(t, err) + require.True(t, mock.ClientStateInactive == state) + + // Manually inform the server that the session is active. + _, err = client.ActivateSession(ctx, pubKey) + require.NoError(t, err) + + // Assert that the server moved the client to the Active state. + state, err = server.GetClientState(pubKey) + require.NoError(t, err) + require.True(t, mock.ClientStateActive == state) + + // Once again, let the server move the client to the inactive state. + err = server.SetClientState(pubKey, mock.ClientStateInactive) + require.NoError(t, err) + state, err = server.GetClientState(pubKey) + require.NoError(t, err) + require.True(t, mock.ClientStateInactive == state) + + // Now wait for client to re-activate the session with the server + err = wait.Predicate(func() bool { + state, err = server.GetClientState(pubKey) + require.NoError(t, err) + return state == mock.ClientStateActive + }, time.Second*5) + require.NoError(t, err) +} diff --git a/autopilotserver/interface.go b/autopilotserver/interface.go new file mode 100644 index 000000000..3c0f4bcbe --- /dev/null +++ b/autopilotserver/interface.go @@ -0,0 +1,80 @@ +package autopilotserver + +import ( + "context" + + "github.com/btcsuite/btcd/btcec/v2" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +// Autopilot represents the functionality exposed by an autopilot server. +type Autopilot interface { + // ListFeatures fetches the set of features offered by the autopilot + // server along with all the rules and permissions required for those + // features. + ListFeatures(ctx context.Context) (map[string]*Feature, error) + + // ListFeaturePerms returns a map of feature names to a map of + // permissions required for each feature. This call uses an in-memory + // store that is updated periodically and so this should be used instead + // of the ListFeatures call if only the permissions are required to + // avoid doing multiple calls to the autopilot server. The ListFeatures + // call can however be used to force the update of the in-memory list. + ListFeaturePerms(ctx context.Context) (map[string]map[string]bool, + error) + + // RegisterSession attempts to register a session with the autopilot + // server. If the registration is successful, then the Client will also + // track the session so that it can continuously ensure that the session + // remains active. + RegisterSession(ctx context.Context, pubKey *btcec.PublicKey, + mailboxAddr string, devServer bool, + featureConf map[string][]byte) (*btcec.PublicKey, error) + + // ActivateSession attempts to inform the autopilot server that the + // given session is still active. After this is called, the autopilot + // client will periodically ensure that the session remains active. + // The boolean returned is true if the error received was permanent + // meaning that the session should be revoked and recreated. + ActivateSession(ctx context.Context, pubKey *btcec.PublicKey) (bool, + error) + + // SessionRevoked should be called when a session is no longer active + // so that the client can forget the session. + SessionRevoked(ctx context.Context, key *btcec.PublicKey) + + // Start kicks off the goroutines of the client. + Start(opts ...func(cfg *Config)) error + + // Stop cleans up any resources held by the client. + Stop() +} + +// Feature holds all the info necessary to subscribe to a feature offered by +// the autopilot server. +type Feature struct { + // Name is the name of the feature. + Name string + + // Description is a human-readable description of what the feature + // offers + Description string + + // Permissions is a list of RPC methods and access writes a feature + // will need. + Permissions map[string][]bakery.Op + + // Rules is a list of all the firewall that must be specified for this + // feature. + Rules map[string]*RuleValues +} + +// RuleValues holds the default value along with the sane max and min values +// that the autopilot server indicates makes sense for feature that the rule is +// being applied to. The values can be unmarshalled in a higher layer if the +// name of the rule is known to LiT. +type RuleValues struct { + Default []byte + MinVal []byte + MaxVal []byte +} diff --git a/autopilotserver/log.go b/autopilotserver/log.go new file mode 100644 index 000000000..ef9c49f13 --- /dev/null +++ b/autopilotserver/log.go @@ -0,0 +1,25 @@ +package autopilotserver + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +const Subsystem = "AUTO" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/autopilotserver/mock/log.go b/autopilotserver/mock/log.go new file mode 100644 index 000000000..00816644b --- /dev/null +++ b/autopilotserver/mock/log.go @@ -0,0 +1,25 @@ +package mock + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +const Subsystem = "AUTO" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/autopilotserver/mock/server.go b/autopilotserver/mock/server.go new file mode 100644 index 000000000..de9d0f1a8 --- /dev/null +++ b/autopilotserver/mock/server.go @@ -0,0 +1,394 @@ +package mock + +import ( + "context" + "encoding/hex" + "fmt" + "net" + "sync" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/lightning-terminal/autopilotserverrpc" + "github.com/lightninglabs/lightning-terminal/rules" + "github.com/lightningnetwork/lnd/lntest" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +// Server implements the autopilotserverrpc.AutopilotServer interface and is +// used to mock the autopilot server in tests. +type Server struct { + autopilotserverrpc.AutopilotServer + + // featureSet is the set of features that the autopilot server supports. + featureSet map[string]*Feature + + // sessions is a map from the public key string of the remote node + // (the litd node in this case) to the private key of the autopilot + // server. + sessions map[string]*clientSession + sessMu sync.Mutex + + // port is the port number on which the mock autopilot server will + // host its grpc service. + port int + + grpcServer *grpc.Server + + wg sync.WaitGroup +} + +type ClientState uint8 + +const ( + ClientStateActive = iota + ClientStateInactive +) + +type clientSession struct { + key *btcec.PrivateKey + state ClientState +} + +// NewServer constructs a new MockAutoPilotServer. +func NewServer() *Server { + return &Server{ + port: lntest.NextAvailablePort(), + sessions: make(map[string]*clientSession), + grpcServer: grpc.NewServer( + grpc.Creds(insecure.NewCredentials()), + ), + featureSet: defaultFeatures, + } +} + +// Start kicks off the mock autopilot grpc server. +func (m *Server) Start() error { + lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", m.port)) + if err != nil { + return err + } + + autopilotserverrpc.RegisterAutopilotServer(m.grpcServer, m) + + m.wg.Add(1) + go func() { + defer m.wg.Done() + + err := m.grpcServer.Serve(lis) + if err != nil && err != grpc.ErrServerStopped { + log.Errorf("RPC server stopped with error: %v", err) + } + }() + + return nil +} + +// Stop cleans up any resources held by the mock server. +func (m *Server) Stop() { + m.grpcServer.Stop() + m.wg.Wait() +} + +// GetPort returns the port number that the mock server is serving its grpc +// server on. +func (m *Server) GetPort() int { + return m.port +} + +// SetFeatures can be used to override the feature set served by the mock +// autopilot server. +func (m *Server) SetFeatures(f map[string]*Feature) { + m.featureSet = f +} + +// Terms returns any meta data from the autopilot server. +// +// Note: this is part of the autopilotrpc.AutopilotServer interface. +func (m *Server) Terms(context.Context, *autopilotserverrpc.TermsRequest) ( + *autopilotserverrpc.TermsResponse, error) { + + return &autopilotserverrpc.TermsResponse{ + MinRequiredVersion: &autopilotserverrpc.Version{ + Major: 0, + Minor: 0, + Patch: 0, + }, + }, nil +} + +// ListFeatures converts the mockFeatures into the form that the autopilot +// server would. +// +// Note: this is part of the autopilotrpc.AutopilotServer interface. +func (m *Server) ListFeatures(_ context.Context, + _ *autopilotserverrpc.ListFeaturesRequest) ( + *autopilotserverrpc.ListFeaturesResponse, error) { + + res := make(map[string]*autopilotserverrpc.Feature, len(m.featureSet)) + for name, f := range m.featureSet { + rules, err := rulesToRPC(f.Rules) + if err != nil { + return nil, err + } + + res[name] = &autopilotserverrpc.Feature{ + Name: name, + Description: f.Description, + Rules: rules, + PermissionsList: permissionsToRPC(f.Permissions), + } + } + + return &autopilotserverrpc.ListFeaturesResponse{ + Features: res, + }, nil +} + +// RegisterSession will create a new priv key for the autopilot server and +// return the corresponding public key. +// +// Note: this is part of the autopilotrpc.AutopilotServer interface. +func (m *Server) RegisterSession(_ context.Context, + req *autopilotserverrpc.RegisterSessionRequest) ( + *autopilotserverrpc.RegisterSessionResponse, error) { + + m.sessMu.Lock() + defer m.sessMu.Unlock() + + _, ok := m.sessions[hex.EncodeToString(req.ResponderPubKey)] + if ok { + return nil, fmt.Errorf("client already registered") + } + + priv, err := btcec.NewPrivateKey() + if err != nil { + return nil, err + } + + m.sessions[hex.EncodeToString(req.ResponderPubKey)] = &clientSession{ + key: priv, + state: ClientStateActive, + } + + return &autopilotserverrpc.RegisterSessionResponse{ + InitiatorPubKey: priv.PubKey().SerializeCompressed(), + }, nil +} + +func (m *Server) ActivateSession(_ context.Context, + req *autopilotserverrpc.ActivateSessionRequest) ( + *autopilotserverrpc.ActivateSessionResponse, error) { + + m.sessMu.Lock() + defer m.sessMu.Unlock() + + session, ok := m.sessions[hex.EncodeToString(req.ResponderPubKey)] + if !ok { + return nil, fmt.Errorf("no such client") + } + + session.state = ClientStateActive + return &autopilotserverrpc.ActivateSessionResponse{}, nil +} + +// RevokeSession revokes a single session and also stops it if it is currently +// active. +// +// Note: this is part of the autopilotrpc.AutopilotServer interface. +func (m *Server) RevokeSession(_ context.Context, + req *autopilotserverrpc.RevokeSessionRequest) ( + *autopilotserverrpc.RevokeSessionResponse, error) { + + m.sessMu.Lock() + defer m.sessMu.Unlock() + + delete(m.sessions, hex.EncodeToString(req.ResponderPubKey)) + + return &autopilotserverrpc.RevokeSessionResponse{}, nil +} + +// GetPrivKey can be used to extract the private key that the autopilot created +// for the given litd static key. +func (m *Server) GetPrivKey(remoteKey *btcec.PublicKey) ( + *btcec.PrivateKey, error) { + + m.sessMu.Lock() + defer m.sessMu.Unlock() + + key := hex.EncodeToString(remoteKey.SerializeCompressed()) + session, ok := m.sessions[key] + if !ok { + return nil, fmt.Errorf("no key found") + } + + return session.key, nil +} + +func (m *Server) GetClientState(remoteKey *btcec.PublicKey) ( + ClientState, error) { + + m.sessMu.Lock() + defer m.sessMu.Unlock() + + key := hex.EncodeToString(remoteKey.SerializeCompressed()) + session, ok := m.sessions[key] + if !ok { + return 0, fmt.Errorf("no such client found") + } + + return session.state, nil +} + +func (m *Server) SetClientState(remoteKey *btcec.PublicKey, + s ClientState) error { + + m.sessMu.Lock() + defer m.sessMu.Unlock() + + key := hex.EncodeToString(remoteKey.SerializeCompressed()) + session, ok := m.sessions[key] + if !ok { + return fmt.Errorf("no such client found") + } + + session.state = s + + return nil +} + +// Feature is a feature that the autopilot server could return. +type Feature struct { + Description string + Rules map[string]*RuleRanges + Permissions map[string][]bakery.Op +} + +// defaultFeatures is an example of a set of features that the autopilot server +// could return. +var defaultFeatures = map[string]*Feature{ + "HealthCheck": { + Description: "check that your node is up", + Rules: map[string]*RuleRanges{ + rules.RateLimitName: rateLimitRule, + }, + Permissions: map[string][]bakery.Op{ + "/lnrpc.Lightning/GetInfo": {{ + Entity: "info", + Action: "read", + }}, + }, + }, + "AutoFees": { + Description: "manages your channel fees", + Rules: map[string]*RuleRanges{ + rules.RateLimitName: rateLimitRule, + }, + Permissions: map[string][]bakery.Op{ + "/lnrpc.Lightning/ListChannels": {{ + Entity: "offchain", + Action: "read", + }}, + "/lnrpc.Lightning/UpdateChannelPolicy": {{ + Entity: "offchain", + Action: "write", + }}, + "/lnrpc.Lightning/FeeReport": {{ + Entity: "offchain", + Action: "read", + }}, + }, + }, +} + +var rateLimitRule = &RuleRanges{ + Default: &rules.RateLimit{ + WriteLimit: &rules.Rate{ + Iterations: 1, + NumHours: 1, + }, + ReadLimit: &rules.Rate{ + Iterations: 10, + NumHours: 1, + }, + }, + MinVal: &rules.RateLimit{ + WriteLimit: &rules.Rate{ + Iterations: 0, + NumHours: 1, + }, + ReadLimit: &rules.Rate{ + Iterations: 1, + NumHours: 1, + }, + }, + MaxVal: &rules.RateLimit{ + WriteLimit: &rules.Rate{ + Iterations: 10, + NumHours: 1, + }, + ReadLimit: &rules.Rate{ + Iterations: 1000, + NumHours: 1, + }, + }, +} + +type RuleRanges struct { + Default rules.Values + MinVal rules.Values + MaxVal rules.Values +} + +func rulesToRPC(rulesMap map[string]*RuleRanges) ( + map[string]*autopilotserverrpc.Rule, error) { + + res := make(map[string]*autopilotserverrpc.Rule, len(rulesMap)) + for name, rule := range rulesMap { + defaultVals, err := rules.Marshal(rule.Default) + if err != nil { + return nil, err + } + + minVals, err := rules.Marshal(rule.MinVal) + if err != nil { + return nil, err + } + + maxVals, err := rules.Marshal(rule.MaxVal) + if err != nil { + return nil, err + } + + res[name] = &autopilotserverrpc.Rule{ + Name: name, + Default: defaultVals, + MinValue: minVals, + MaxValue: maxVals, + } + } + + return res, nil +} + +func permissionsToRPC(ps map[string][]bakery.Op) []*autopilotserverrpc.Permissions { + res := make([]*autopilotserverrpc.Permissions, len(ps)) + + for method, ops := range ps { + operations := make([]*autopilotserverrpc.Operation, len(ops)) + for i, op := range ops { + operations[i] = &autopilotserverrpc.Operation{ + Entity: op.Entity, + Action: op.Action, + } + } + + res = append(res, &autopilotserverrpc.Permissions{ + Method: method, + Operations: operations, + }) + } + + return res +} diff --git a/autopilotserverrpc/.clang-format b/autopilotserverrpc/.clang-format new file mode 100644 index 000000000..f19142787 --- /dev/null +++ b/autopilotserverrpc/.clang-format @@ -0,0 +1,7 @@ +--- +Language: Proto +BasedOnStyle: Google +IndentWidth: 4 +AllowShortFunctionsOnASingleLine: None +SpaceBeforeParens: Always +CompactNamespaces: false diff --git a/autopilotserverrpc/autopilotserver.pb.go b/autopilotserverrpc/autopilotserver.pb.go new file mode 100644 index 000000000..f83a72a8f --- /dev/null +++ b/autopilotserverrpc/autopilotserver.pb.go @@ -0,0 +1,1302 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.6.1 +// source: autopilotserver.proto + +package autopilotserverrpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TermsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TermsRequest) Reset() { + *x = TermsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TermsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TermsRequest) ProtoMessage() {} + +func (x *TermsRequest) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TermsRequest.ProtoReflect.Descriptor instead. +func (*TermsRequest) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{0} +} + +type TermsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MinRequiredVersion *Version `protobuf:"bytes,1,opt,name=min_required_version,json=minRequiredVersion,proto3" json:"min_required_version,omitempty"` +} + +func (x *TermsResponse) Reset() { + *x = TermsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TermsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TermsResponse) ProtoMessage() {} + +func (x *TermsResponse) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TermsResponse.ProtoReflect.Descriptor instead. +func (*TermsResponse) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{1} +} + +func (x *TermsResponse) GetMinRequiredVersion() *Version { + if x != nil { + return x.MinRequiredVersion + } + return nil +} + +type Version struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The major application version. + Major uint32 `protobuf:"varint,1,opt,name=major,proto3" json:"major,omitempty"` + // + //The minor application version. + Minor uint32 `protobuf:"varint,2,opt,name=minor,proto3" json:"minor,omitempty"` + // + //The application patch number. + Patch uint32 `protobuf:"varint,3,opt,name=patch,proto3" json:"patch,omitempty"` +} + +func (x *Version) Reset() { + *x = Version{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Version) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Version) ProtoMessage() {} + +func (x *Version) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Version.ProtoReflect.Descriptor instead. +func (*Version) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{2} +} + +func (x *Version) GetMajor() uint32 { + if x != nil { + return x.Major + } + return 0 +} + +func (x *Version) GetMinor() uint32 { + if x != nil { + return x.Minor + } + return 0 +} + +func (x *Version) GetPatch() uint32 { + if x != nil { + return x.Patch + } + return 0 +} + +type ListFeaturesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ListFeaturesRequest) Reset() { + *x = ListFeaturesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListFeaturesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListFeaturesRequest) ProtoMessage() {} + +func (x *ListFeaturesRequest) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListFeaturesRequest.ProtoReflect.Descriptor instead. +func (*ListFeaturesRequest) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{3} +} + +type ActivateSessionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The static public key of the client that is to be used for future noise + //handshakes. Since the autopilot is the initiator of a connection, the + //client's key is called the "responder" key. + ResponderPubKey []byte `protobuf:"bytes,1,opt,name=responder_pub_key,json=responderPubKey,proto3" json:"responder_pub_key,omitempty"` +} + +func (x *ActivateSessionRequest) Reset() { + *x = ActivateSessionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ActivateSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActivateSessionRequest) ProtoMessage() {} + +func (x *ActivateSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ActivateSessionRequest.ProtoReflect.Descriptor instead. +func (*ActivateSessionRequest) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{4} +} + +func (x *ActivateSessionRequest) GetResponderPubKey() []byte { + if x != nil { + return x.ResponderPubKey + } + return nil +} + +type ActivateSessionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The autopilot's static pub key to be used for the noise connection with the + //client. + InitiatorPubKey []byte `protobuf:"bytes,1,opt,name=initiator_pub_key,json=initiatorPubKey,proto3" json:"initiator_pub_key,omitempty"` +} + +func (x *ActivateSessionResponse) Reset() { + *x = ActivateSessionResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ActivateSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActivateSessionResponse) ProtoMessage() {} + +func (x *ActivateSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ActivateSessionResponse.ProtoReflect.Descriptor instead. +func (*ActivateSessionResponse) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{5} +} + +func (x *ActivateSessionResponse) GetInitiatorPubKey() []byte { + if x != nil { + return x.InitiatorPubKey + } + return nil +} + +type ListFeaturesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //A map of feature name to Feature object. This map represents each of the + //features supported by the autopilot server along with the details of + //each feature. + Features map[string]*Feature `protobuf:"bytes,1,rep,name=features,proto3" json:"features,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *ListFeaturesResponse) Reset() { + *x = ListFeaturesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListFeaturesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListFeaturesResponse) ProtoMessage() {} + +func (x *ListFeaturesResponse) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListFeaturesResponse.ProtoReflect.Descriptor instead. +func (*ListFeaturesResponse) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{6} +} + +func (x *ListFeaturesResponse) GetFeatures() map[string]*Feature { + if x != nil { + return x.Features + } + return nil +} + +type Feature struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The string identifier of the feature. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // + //A human-readable description of what the feature offers. + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // + //A map of the rule names to rule values that make sense for this feature. + Rules map[string]*Rule `protobuf:"bytes,3,rep,name=rules,proto3" json:"rules,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // + //A list of the permissions that the feature will require to operate. + PermissionsList []*Permissions `protobuf:"bytes,4,rep,name=permissions_list,json=permissionsList,proto3" json:"permissions_list,omitempty"` +} + +func (x *Feature) Reset() { + *x = Feature{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Feature) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Feature) ProtoMessage() {} + +func (x *Feature) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Feature.ProtoReflect.Descriptor instead. +func (*Feature) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{7} +} + +func (x *Feature) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Feature) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Feature) GetRules() map[string]*Rule { + if x != nil { + return x.Rules + } + return nil +} + +func (x *Feature) GetPermissionsList() []*Permissions { + if x != nil { + return x.PermissionsList + } + return nil +} + +type Rule struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The name of the rule. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // + //The recommended default values for the rule. These may vary from feature + //to feature. + Default []byte `protobuf:"bytes,2,opt,name=default,proto3" json:"default,omitempty"` + // + //The minimum sane value of the rule that is allowed for the associated + //feature. + MinValue []byte `protobuf:"bytes,3,opt,name=min_value,json=minValue,proto3" json:"min_value,omitempty"` + // + //The maximum sane value of the rule that is allowed for the associated + //feature. + MaxValue []byte `protobuf:"bytes,4,opt,name=max_value,json=maxValue,proto3" json:"max_value,omitempty"` +} + +func (x *Rule) Reset() { + *x = Rule{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Rule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Rule) ProtoMessage() {} + +func (x *Rule) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Rule.ProtoReflect.Descriptor instead. +func (*Rule) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{8} +} + +func (x *Rule) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Rule) GetDefault() []byte { + if x != nil { + return x.Default + } + return nil +} + +func (x *Rule) GetMinValue() []byte { + if x != nil { + return x.MinValue + } + return nil +} + +func (x *Rule) GetMaxValue() []byte { + if x != nil { + return x.MaxValue + } + return nil +} + +type Permissions struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The URI of the method that the operation set is referring to. + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + // + //A list of operations that the method can perform + Operations []*Operation `protobuf:"bytes,2,rep,name=operations,proto3" json:"operations,omitempty"` +} + +func (x *Permissions) Reset() { + *x = Permissions{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Permissions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Permissions) ProtoMessage() {} + +func (x *Permissions) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Permissions.ProtoReflect.Descriptor instead. +func (*Permissions) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{9} +} + +func (x *Permissions) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *Permissions) GetOperations() []*Operation { + if x != nil { + return x.Operations + } + return nil +} + +type Operation struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The entity to which the action applies. + Entity string `protobuf:"bytes,1,opt,name=entity,proto3" json:"entity,omitempty"` + // + //The action that can be performed on the above specified entity. + Action string `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"` +} + +func (x *Operation) Reset() { + *x = Operation{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Operation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Operation) ProtoMessage() {} + +func (x *Operation) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Operation.ProtoReflect.Descriptor instead. +func (*Operation) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{10} +} + +func (x *Operation) GetEntity() string { + if x != nil { + return x.Entity + } + return "" +} + +func (x *Operation) GetAction() string { + if x != nil { + return x.Action + } + return "" +} + +type RegisterSessionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The static public key of the client that is to be used for future noise + //handshakes. Since the autopilot is the initiator of a connection, the + //client's key is called the "responder" key. + ResponderPubKey []byte `protobuf:"bytes,1,opt,name=responder_pub_key,json=responderPubKey,proto3" json:"responder_pub_key,omitempty"` + // + //The address of the mailbox that the client will use for the LNC connection. + MailboxAddr string `protobuf:"bytes,2,opt,name=mailbox_addr,json=mailboxAddr,proto3" json:"mailbox_addr,omitempty"` + // + //Set to true if tls should be skipped for when connecting to the mailbox. + DevServer bool `protobuf:"varint,3,opt,name=dev_server,json=devServer,proto3" json:"dev_server,omitempty"` + // + //A map from feature name to configuration bytes for that feature. + FeatureConfigs map[string][]byte `protobuf:"bytes,4,rep,name=feature_configs,json=featureConfigs,proto3" json:"feature_configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // + //The version of the Lit client. + LitVersion *Version `protobuf:"bytes,5,opt,name=lit_version,json=litVersion,proto3" json:"lit_version,omitempty"` + // + //The version of the LND that the Lit client is using. + LndVersion *Version `protobuf:"bytes,6,opt,name=lnd_version,json=lndVersion,proto3" json:"lnd_version,omitempty"` +} + +func (x *RegisterSessionRequest) Reset() { + *x = RegisterSessionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RegisterSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterSessionRequest) ProtoMessage() {} + +func (x *RegisterSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterSessionRequest.ProtoReflect.Descriptor instead. +func (*RegisterSessionRequest) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{11} +} + +func (x *RegisterSessionRequest) GetResponderPubKey() []byte { + if x != nil { + return x.ResponderPubKey + } + return nil +} + +func (x *RegisterSessionRequest) GetMailboxAddr() string { + if x != nil { + return x.MailboxAddr + } + return "" +} + +func (x *RegisterSessionRequest) GetDevServer() bool { + if x != nil { + return x.DevServer + } + return false +} + +func (x *RegisterSessionRequest) GetFeatureConfigs() map[string][]byte { + if x != nil { + return x.FeatureConfigs + } + return nil +} + +func (x *RegisterSessionRequest) GetLitVersion() *Version { + if x != nil { + return x.LitVersion + } + return nil +} + +func (x *RegisterSessionRequest) GetLndVersion() *Version { + if x != nil { + return x.LndVersion + } + return nil +} + +type RegisterSessionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The autopilot's static pub key to be used for the noise connection with the + //client. + InitiatorPubKey []byte `protobuf:"bytes,1,opt,name=initiator_pub_key,json=initiatorPubKey,proto3" json:"initiator_pub_key,omitempty"` +} + +func (x *RegisterSessionResponse) Reset() { + *x = RegisterSessionResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RegisterSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterSessionResponse) ProtoMessage() {} + +func (x *RegisterSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterSessionResponse.ProtoReflect.Descriptor instead. +func (*RegisterSessionResponse) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{12} +} + +func (x *RegisterSessionResponse) GetInitiatorPubKey() []byte { + if x != nil { + return x.InitiatorPubKey + } + return nil +} + +type RevokeSessionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The client's key for the session that the client is revoking. + ResponderPubKey []byte `protobuf:"bytes,1,opt,name=responder_pub_key,json=responderPubKey,proto3" json:"responder_pub_key,omitempty"` +} + +func (x *RevokeSessionRequest) Reset() { + *x = RevokeSessionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RevokeSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeSessionRequest) ProtoMessage() {} + +func (x *RevokeSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeSessionRequest.ProtoReflect.Descriptor instead. +func (*RevokeSessionRequest) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{13} +} + +func (x *RevokeSessionRequest) GetResponderPubKey() []byte { + if x != nil { + return x.ResponderPubKey + } + return nil +} + +type RevokeSessionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *RevokeSessionResponse) Reset() { + *x = RevokeSessionResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_autopilotserver_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RevokeSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeSessionResponse) ProtoMessage() {} + +func (x *RevokeSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_autopilotserver_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeSessionResponse.ProtoReflect.Descriptor instead. +func (*RevokeSessionResponse) Descriptor() ([]byte, []int) { + return file_autopilotserver_proto_rawDescGZIP(), []int{14} +} + +var File_autopilotserver_proto protoreflect.FileDescriptor + +var file_autopilotserver_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, + 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x22, 0x0e, 0x0a, 0x0c, 0x54, + 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5e, 0x0a, 0x0d, 0x54, + 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x14, + 0x6d, 0x69, 0x6e, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x75, 0x74, + 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x12, 0x6d, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x4b, 0x0a, 0x07, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x61, 0x6a, 0x6f, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6d, 0x61, 0x6a, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, + 0x6d, 0x69, 0x6e, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6d, 0x69, 0x6e, + 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x05, 0x70, 0x61, 0x74, 0x63, 0x68, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, + 0x44, 0x0a, 0x16, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x65, 0x72, 0x50, + 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x45, 0x0a, 0x17, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2a, 0x0a, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x70, 0x75, + 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x69, 0x6e, 0x69, + 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0xc4, 0x01, 0x0a, + 0x14, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, + 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x1a, 0x58, 0x0a, 0x0d, 0x46, 0x65, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x75, + 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, + 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x9d, 0x02, 0x0a, 0x07, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x72, 0x75, + 0x6c, 0x65, 0x73, 0x12, 0x4a, 0x0a, 0x10, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, + 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, + 0x70, 0x63, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0f, + 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x1a, + 0x52, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x2e, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, + 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x6e, 0x0a, 0x04, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6e, + 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6d, 0x69, + 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x22, 0x64, 0x0a, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x3d, 0x0a, 0x0a, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3b, 0x0a, 0x09, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x16, + 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xae, 0x03, 0x0a, 0x16, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x70, + 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x65, 0x72, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, + 0x0c, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x41, 0x64, 0x64, 0x72, + 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x76, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x64, 0x65, 0x76, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, + 0x67, 0x0a, 0x0f, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x70, + 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x3c, 0x0a, 0x0b, 0x6c, 0x69, 0x74, 0x5f, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, + 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, + 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6c, 0x69, 0x74, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0b, 0x6c, 0x6e, 0x64, 0x5f, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x75, + 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, + 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6c, 0x6e, 0x64, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x41, 0x0a, 0x13, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x45, 0x0a, 0x17, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x5f, + 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x69, + 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x42, + 0x0a, 0x14, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x64, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x65, 0x72, 0x50, 0x75, 0x62, 0x4b, + 0x65, 0x79, 0x22, 0x17, 0x0a, 0x15, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xfa, 0x03, 0x0a, 0x09, + 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x12, 0x4c, 0x0a, 0x05, 0x54, 0x65, 0x72, + 0x6d, 0x73, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x46, + 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, + 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x28, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x0f, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2a, 0x2e, + 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, + 0x70, 0x63, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x6f, + 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x0f, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2a, 0x2e, 0x61, 0x75, 0x74, 0x6f, + 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x41, + 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, + 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x64, 0x0a, 0x0d, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, + 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, + 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, + 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x74, + 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, + 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_autopilotserver_proto_rawDescOnce sync.Once + file_autopilotserver_proto_rawDescData = file_autopilotserver_proto_rawDesc +) + +func file_autopilotserver_proto_rawDescGZIP() []byte { + file_autopilotserver_proto_rawDescOnce.Do(func() { + file_autopilotserver_proto_rawDescData = protoimpl.X.CompressGZIP(file_autopilotserver_proto_rawDescData) + }) + return file_autopilotserver_proto_rawDescData +} + +var file_autopilotserver_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_autopilotserver_proto_goTypes = []interface{}{ + (*TermsRequest)(nil), // 0: autopilotserverrpc.TermsRequest + (*TermsResponse)(nil), // 1: autopilotserverrpc.TermsResponse + (*Version)(nil), // 2: autopilotserverrpc.Version + (*ListFeaturesRequest)(nil), // 3: autopilotserverrpc.ListFeaturesRequest + (*ActivateSessionRequest)(nil), // 4: autopilotserverrpc.ActivateSessionRequest + (*ActivateSessionResponse)(nil), // 5: autopilotserverrpc.ActivateSessionResponse + (*ListFeaturesResponse)(nil), // 6: autopilotserverrpc.ListFeaturesResponse + (*Feature)(nil), // 7: autopilotserverrpc.Feature + (*Rule)(nil), // 8: autopilotserverrpc.Rule + (*Permissions)(nil), // 9: autopilotserverrpc.Permissions + (*Operation)(nil), // 10: autopilotserverrpc.Operation + (*RegisterSessionRequest)(nil), // 11: autopilotserverrpc.RegisterSessionRequest + (*RegisterSessionResponse)(nil), // 12: autopilotserverrpc.RegisterSessionResponse + (*RevokeSessionRequest)(nil), // 13: autopilotserverrpc.RevokeSessionRequest + (*RevokeSessionResponse)(nil), // 14: autopilotserverrpc.RevokeSessionResponse + nil, // 15: autopilotserverrpc.ListFeaturesResponse.FeaturesEntry + nil, // 16: autopilotserverrpc.Feature.RulesEntry + nil, // 17: autopilotserverrpc.RegisterSessionRequest.FeatureConfigsEntry +} +var file_autopilotserver_proto_depIdxs = []int32{ + 2, // 0: autopilotserverrpc.TermsResponse.min_required_version:type_name -> autopilotserverrpc.Version + 15, // 1: autopilotserverrpc.ListFeaturesResponse.features:type_name -> autopilotserverrpc.ListFeaturesResponse.FeaturesEntry + 16, // 2: autopilotserverrpc.Feature.rules:type_name -> autopilotserverrpc.Feature.RulesEntry + 9, // 3: autopilotserverrpc.Feature.permissions_list:type_name -> autopilotserverrpc.Permissions + 10, // 4: autopilotserverrpc.Permissions.operations:type_name -> autopilotserverrpc.Operation + 17, // 5: autopilotserverrpc.RegisterSessionRequest.feature_configs:type_name -> autopilotserverrpc.RegisterSessionRequest.FeatureConfigsEntry + 2, // 6: autopilotserverrpc.RegisterSessionRequest.lit_version:type_name -> autopilotserverrpc.Version + 2, // 7: autopilotserverrpc.RegisterSessionRequest.lnd_version:type_name -> autopilotserverrpc.Version + 7, // 8: autopilotserverrpc.ListFeaturesResponse.FeaturesEntry.value:type_name -> autopilotserverrpc.Feature + 8, // 9: autopilotserverrpc.Feature.RulesEntry.value:type_name -> autopilotserverrpc.Rule + 0, // 10: autopilotserverrpc.Autopilot.Terms:input_type -> autopilotserverrpc.TermsRequest + 3, // 11: autopilotserverrpc.Autopilot.ListFeatures:input_type -> autopilotserverrpc.ListFeaturesRequest + 11, // 12: autopilotserverrpc.Autopilot.RegisterSession:input_type -> autopilotserverrpc.RegisterSessionRequest + 4, // 13: autopilotserverrpc.Autopilot.ActivateSession:input_type -> autopilotserverrpc.ActivateSessionRequest + 13, // 14: autopilotserverrpc.Autopilot.RevokeSession:input_type -> autopilotserverrpc.RevokeSessionRequest + 1, // 15: autopilotserverrpc.Autopilot.Terms:output_type -> autopilotserverrpc.TermsResponse + 6, // 16: autopilotserverrpc.Autopilot.ListFeatures:output_type -> autopilotserverrpc.ListFeaturesResponse + 12, // 17: autopilotserverrpc.Autopilot.RegisterSession:output_type -> autopilotserverrpc.RegisterSessionResponse + 5, // 18: autopilotserverrpc.Autopilot.ActivateSession:output_type -> autopilotserverrpc.ActivateSessionResponse + 14, // 19: autopilotserverrpc.Autopilot.RevokeSession:output_type -> autopilotserverrpc.RevokeSessionResponse + 15, // [15:20] is the sub-list for method output_type + 10, // [10:15] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name +} + +func init() { file_autopilotserver_proto_init() } +func file_autopilotserver_proto_init() { + if File_autopilotserver_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_autopilotserver_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TermsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TermsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Version); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListFeaturesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ActivateSessionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ActivateSessionResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListFeaturesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Feature); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Rule); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Permissions); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Operation); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RegisterSessionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RegisterSessionResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RevokeSessionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_autopilotserver_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RevokeSessionResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_autopilotserver_proto_rawDesc, + NumEnums: 0, + NumMessages: 18, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_autopilotserver_proto_goTypes, + DependencyIndexes: file_autopilotserver_proto_depIdxs, + MessageInfos: file_autopilotserver_proto_msgTypes, + }.Build() + File_autopilotserver_proto = out.File + file_autopilotserver_proto_rawDesc = nil + file_autopilotserver_proto_goTypes = nil + file_autopilotserver_proto_depIdxs = nil +} diff --git a/autopilotserverrpc/autopilotserver.proto b/autopilotserverrpc/autopilotserver.proto new file mode 100644 index 000000000..8bb8bce67 --- /dev/null +++ b/autopilotserverrpc/autopilotserver.proto @@ -0,0 +1,191 @@ +syntax = "proto3"; + +package autopilotserverrpc; + +option go_package = "github.com/lightninglabs/lightning-terminal/autopilotserverrpc"; + +service Autopilot { + rpc Terms (TermsRequest) returns (TermsResponse); + rpc ListFeatures (ListFeaturesRequest) returns (ListFeaturesResponse); + rpc RegisterSession (RegisterSessionRequest) + returns (RegisterSessionResponse); + rpc ActivateSession (ActivateSessionRequest) + returns (ActivateSessionResponse); + rpc RevokeSession (RevokeSessionRequest) returns (RevokeSessionResponse); +} + +message TermsRequest { +} + +message TermsResponse { + Version min_required_version = 1; +} + +message Version { + /* + The major application version. + */ + uint32 major = 1; + + /* + The minor application version. + */ + uint32 minor = 2; + + /* + The application patch number. + */ + uint32 patch = 3; +} + +message ListFeaturesRequest { +} + +message ActivateSessionRequest { + /* + The static public key of the client that is to be used for future noise + handshakes. Since the autopilot is the initiator of a connection, the + client's key is called the "responder" key. + */ + bytes responder_pub_key = 1; +} + +message ActivateSessionResponse { + /* + The autopilot's static pub key to be used for the noise connection with the + client. + */ + bytes initiator_pub_key = 1; +} + +message ListFeaturesResponse { + /* + A map of feature name to Feature object. This map represents each of the + features supported by the autopilot server along with the details of + each feature. + */ + map features = 1; +} + +message Feature { + /* + The string identifier of the feature. + */ + string name = 1; + + /* + A human-readable description of what the feature offers. + */ + string description = 2; + + /* + A map of the rule names to rule values that make sense for this feature. + */ + map rules = 3; + + /* + A list of the permissions that the feature will require to operate. + */ + repeated Permissions permissions_list = 4; +} + +message Rule { + /* + The name of the rule. + */ + string name = 1; + + /* + The recommended default values for the rule. These may vary from feature + to feature. + */ + bytes default = 2; + + /* + The minimum sane value of the rule that is allowed for the associated + feature. + */ + bytes min_value = 3; + + /* + The maximum sane value of the rule that is allowed for the associated + feature. + */ + bytes max_value = 4; +} + +message Permissions { + /* + The URI of the method that the operation set is referring to. + */ + string method = 1; + + /* + A list of operations that the method can perform + */ + repeated Operation operations = 2; +} + +message Operation { + /* + The entity to which the action applies. + */ + string entity = 1; + + /* + The action that can be performed on the above specified entity. + */ + string action = 2; +} + +message RegisterSessionRequest { + /* + The static public key of the client that is to be used for future noise + handshakes. Since the autopilot is the initiator of a connection, the + client's key is called the "responder" key. + */ + bytes responder_pub_key = 1; + + /* + The address of the mailbox that the client will use for the LNC connection. + */ + string mailbox_addr = 2; + + /* + Set to true if tls should be skipped for when connecting to the mailbox. + */ + bool dev_server = 3; + + /* + A map from feature name to configuration bytes for that feature. + */ + map feature_configs = 4; + + /* + The version of the Lit client. + */ + Version lit_version = 5; + + /* + The version of the LND that the Lit client is using. + */ + Version lnd_version = 6; +} + +message RegisterSessionResponse { + /* + The autopilot's static pub key to be used for the noise connection with the + client. + */ + bytes initiator_pub_key = 1; +} + +message RevokeSessionRequest { + /* + The client's key for the session that the client is revoking. + */ + bytes responder_pub_key = 1; +} + +message RevokeSessionResponse { +} diff --git a/autopilotserverrpc/autopilotserver_grpc.pb.go b/autopilotserverrpc/autopilotserver_grpc.pb.go new file mode 100644 index 000000000..dd228b964 --- /dev/null +++ b/autopilotserverrpc/autopilotserver_grpc.pb.go @@ -0,0 +1,245 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package autopilotserverrpc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// AutopilotClient is the client API for Autopilot service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AutopilotClient interface { + Terms(ctx context.Context, in *TermsRequest, opts ...grpc.CallOption) (*TermsResponse, error) + ListFeatures(ctx context.Context, in *ListFeaturesRequest, opts ...grpc.CallOption) (*ListFeaturesResponse, error) + RegisterSession(ctx context.Context, in *RegisterSessionRequest, opts ...grpc.CallOption) (*RegisterSessionResponse, error) + ActivateSession(ctx context.Context, in *ActivateSessionRequest, opts ...grpc.CallOption) (*ActivateSessionResponse, error) + RevokeSession(ctx context.Context, in *RevokeSessionRequest, opts ...grpc.CallOption) (*RevokeSessionResponse, error) +} + +type autopilotClient struct { + cc grpc.ClientConnInterface +} + +func NewAutopilotClient(cc grpc.ClientConnInterface) AutopilotClient { + return &autopilotClient{cc} +} + +func (c *autopilotClient) Terms(ctx context.Context, in *TermsRequest, opts ...grpc.CallOption) (*TermsResponse, error) { + out := new(TermsResponse) + err := c.cc.Invoke(ctx, "/autopilotserverrpc.Autopilot/Terms", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *autopilotClient) ListFeatures(ctx context.Context, in *ListFeaturesRequest, opts ...grpc.CallOption) (*ListFeaturesResponse, error) { + out := new(ListFeaturesResponse) + err := c.cc.Invoke(ctx, "/autopilotserverrpc.Autopilot/ListFeatures", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *autopilotClient) RegisterSession(ctx context.Context, in *RegisterSessionRequest, opts ...grpc.CallOption) (*RegisterSessionResponse, error) { + out := new(RegisterSessionResponse) + err := c.cc.Invoke(ctx, "/autopilotserverrpc.Autopilot/RegisterSession", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *autopilotClient) ActivateSession(ctx context.Context, in *ActivateSessionRequest, opts ...grpc.CallOption) (*ActivateSessionResponse, error) { + out := new(ActivateSessionResponse) + err := c.cc.Invoke(ctx, "/autopilotserverrpc.Autopilot/ActivateSession", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *autopilotClient) RevokeSession(ctx context.Context, in *RevokeSessionRequest, opts ...grpc.CallOption) (*RevokeSessionResponse, error) { + out := new(RevokeSessionResponse) + err := c.cc.Invoke(ctx, "/autopilotserverrpc.Autopilot/RevokeSession", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AutopilotServer is the server API for Autopilot service. +// All implementations must embed UnimplementedAutopilotServer +// for forward compatibility +type AutopilotServer interface { + Terms(context.Context, *TermsRequest) (*TermsResponse, error) + ListFeatures(context.Context, *ListFeaturesRequest) (*ListFeaturesResponse, error) + RegisterSession(context.Context, *RegisterSessionRequest) (*RegisterSessionResponse, error) + ActivateSession(context.Context, *ActivateSessionRequest) (*ActivateSessionResponse, error) + RevokeSession(context.Context, *RevokeSessionRequest) (*RevokeSessionResponse, error) + mustEmbedUnimplementedAutopilotServer() +} + +// UnimplementedAutopilotServer must be embedded to have forward compatible implementations. +type UnimplementedAutopilotServer struct { +} + +func (UnimplementedAutopilotServer) Terms(context.Context, *TermsRequest) (*TermsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Terms not implemented") +} +func (UnimplementedAutopilotServer) ListFeatures(context.Context, *ListFeaturesRequest) (*ListFeaturesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListFeatures not implemented") +} +func (UnimplementedAutopilotServer) RegisterSession(context.Context, *RegisterSessionRequest) (*RegisterSessionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RegisterSession not implemented") +} +func (UnimplementedAutopilotServer) ActivateSession(context.Context, *ActivateSessionRequest) (*ActivateSessionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ActivateSession not implemented") +} +func (UnimplementedAutopilotServer) RevokeSession(context.Context, *RevokeSessionRequest) (*RevokeSessionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RevokeSession not implemented") +} +func (UnimplementedAutopilotServer) mustEmbedUnimplementedAutopilotServer() {} + +// UnsafeAutopilotServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AutopilotServer will +// result in compilation errors. +type UnsafeAutopilotServer interface { + mustEmbedUnimplementedAutopilotServer() +} + +func RegisterAutopilotServer(s grpc.ServiceRegistrar, srv AutopilotServer) { + s.RegisterService(&Autopilot_ServiceDesc, srv) +} + +func _Autopilot_Terms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TermsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AutopilotServer).Terms(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/autopilotserverrpc.Autopilot/Terms", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AutopilotServer).Terms(ctx, req.(*TermsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Autopilot_ListFeatures_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListFeaturesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AutopilotServer).ListFeatures(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/autopilotserverrpc.Autopilot/ListFeatures", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AutopilotServer).ListFeatures(ctx, req.(*ListFeaturesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Autopilot_RegisterSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AutopilotServer).RegisterSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/autopilotserverrpc.Autopilot/RegisterSession", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AutopilotServer).RegisterSession(ctx, req.(*RegisterSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Autopilot_ActivateSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ActivateSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AutopilotServer).ActivateSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/autopilotserverrpc.Autopilot/ActivateSession", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AutopilotServer).ActivateSession(ctx, req.(*ActivateSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Autopilot_RevokeSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RevokeSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AutopilotServer).RevokeSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/autopilotserverrpc.Autopilot/RevokeSession", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AutopilotServer).RevokeSession(ctx, req.(*RevokeSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Autopilot_ServiceDesc is the grpc.ServiceDesc for Autopilot service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Autopilot_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "autopilotserverrpc.Autopilot", + HandlerType: (*AutopilotServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Terms", + Handler: _Autopilot_Terms_Handler, + }, + { + MethodName: "ListFeatures", + Handler: _Autopilot_ListFeatures_Handler, + }, + { + MethodName: "RegisterSession", + Handler: _Autopilot_RegisterSession_Handler, + }, + { + MethodName: "ActivateSession", + Handler: _Autopilot_ActivateSession_Handler, + }, + { + MethodName: "RevokeSession", + Handler: _Autopilot_RevokeSession_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "autopilotserver.proto", +} diff --git a/autopilotserverrpc/go.mod b/autopilotserverrpc/go.mod new file mode 100644 index 000000000..577686c98 --- /dev/null +++ b/autopilotserverrpc/go.mod @@ -0,0 +1,16 @@ +module github.com/lightninglabs/lightning-terminal/autopilotserverrpc + +go 1.18 + +require ( + google.golang.org/grpc v1.39.0 + google.golang.org/protobuf v1.28.1 +) + +require ( + github.com/golang/protobuf v1.5.2 // indirect + golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect + golang.org/x/text v0.3.3 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect +) diff --git a/autopilotserverrpc/go.sum b/autopilotserverrpc/go.sum new file mode 100644 index 000000000..29d043f81 --- /dev/null +++ b/autopilotserverrpc/go.sum @@ -0,0 +1,120 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/cmd/litcli/actions.go b/cmd/litcli/actions.go new file mode 100644 index 000000000..d520c4522 --- /dev/null +++ b/cmd/litcli/actions.go @@ -0,0 +1,150 @@ +package main + +import ( + "context" + "encoding/hex" + "fmt" + + "github.com/lightninglabs/lightning-terminal/litrpc" + "github.com/urfave/cli" +) + +var listActionsCommand = cli.Command{ + Name: "actions", + Usage: "List actions performed on the Litd server", + Action: listActions, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "feature", + Usage: "The name of the feature to " + + "filter the actions by. If " + + "left empty, then all " + + "actions will be returned.", + }, + cli.StringFlag{ + Name: "actor", + Usage: "The actor name to filter the actions by. If " + + "left empty, then all actions will be " + + "returned.", + }, + cli.StringFlag{ + Name: "method", + Usage: "The method name to filter the actions by. If " + + "left empty, then all actions will be " + + "returned.", + }, + cli.StringFlag{ + Name: "session_id", + Usage: "The session ID to filter the actions by. If " + + "left empty, then all actions will be " + + "returned.", + }, + cli.Uint64Flag{ + Name: "start_timestamp", + Usage: "Only actions executed after this unix " + + "timestamp will be considered.", + }, + cli.Uint64Flag{ + Name: "end_timestamp", + Usage: "Only actions executed before this unix " + + "timestamp will be considered.", + }, + cli.StringFlag{ + Name: "state", + Usage: "The action state to filter on. If not set, " + + "then actions of any state will be returned. " + + "Options include: 'pending', 'done' and 'error", + }, + cli.Uint64Flag{ + Name: "index_offset", + Usage: "The index of an action that will be used as " + + "either the start a query to determine which " + + "actions should be returned in the response", + }, + cli.Uint64Flag{ + Name: "max_num_actions", + Usage: "The max number of actions to return", + }, + cli.BoolFlag{ + Name: "oldest_first", + Usage: "Tf set, actions succeeding the index_offset " + + "will be returned", + }, + cli.BoolFlag{ + Name: "count_total", + Usage: "Set to true if the total number of all " + + "actions that match the given filters should " + + "be counted and returned in the request. " + + "Note that setting this will significantly " + + "decrease the performance of the query if " + + "there are many actions in the db. Also note " + + "that if this option is set, the " + + "index_offset is the index in this set of " + + "actions where as if it is not set, then " + + "index_offset is the index of the action in " + + "the db regardless of filter.", + }, + }, +} + +func listActions(ctx *cli.Context) error { + ctxb := context.Background() + clientConn, cleanup, err := connectClient(ctx) + if err != nil { + return err + } + defer cleanup() + client := litrpc.NewFirewallClient(clientConn) + + state, err := parseActionState(ctx.String("state")) + if err != nil { + return err + } + + var sessionID []byte + if ctx.String("session_id") != "" { + sessionID, err = hex.DecodeString(ctx.String("session_id")) + if err != nil { + return err + } + } + + resp, err := client.ListActions( + ctxb, &litrpc.ListActionsRequest{ + SessionId: sessionID, + FeatureName: ctx.String("feature"), + ActorName: ctx.String("actor"), + MethodName: ctx.String("method"), + State: state, + IndexOffset: ctx.Uint64("index_offset"), + MaxNumActions: ctx.Uint64("max_num_actions"), + Reversed: !ctx.Bool("oldest_first"), + CountTotal: ctx.Bool("count_total"), + StartTimestamp: ctx.Uint64("start_timestamp"), + EndTimestamp: ctx.Uint64("end_timestamp"), + }, + ) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} + +func parseActionState(actionStr string) (litrpc.ActionState, error) { + switch actionStr { + case "": + return litrpc.ActionState_STATE_UNKNOWN, nil + case "pending": + return litrpc.ActionState_STATE_PENDING, nil + case "done": + return litrpc.ActionState_STATE_DONE, nil + case "error": + return litrpc.ActionState_STATE_ERROR, nil + default: + return 0, fmt.Errorf("unknown action state %s. Valid options "+ + "include 'pending', 'done' and 'error'", actionStr) + } +} diff --git a/cmd/litcli/autopilot.go b/cmd/litcli/autopilot.go new file mode 100644 index 000000000..333118008 --- /dev/null +++ b/cmd/litcli/autopilot.go @@ -0,0 +1,197 @@ +package main + +import ( + "context" + "encoding/hex" + "strconv" + "strings" + "time" + + "github.com/lightninglabs/lightning-terminal/litrpc" + "github.com/lightninglabs/lightning-terminal/rules" + "github.com/urfave/cli" +) + +var autopilotCommands = cli.Command{ + Name: "autopilot", + Usage: "manage autopilot sessions", + Category: "Autopilot", + Subcommands: []cli.Command{ + { + Name: "features", + ShortName: "f", + Usage: "List available features", + Action: listFeatures, + }, + { + Name: "add", + ShortName: "a", + Usage: "Initialize an autopilot session", + Action: initAutopilotSession, + Flags: []cli.Flag{ + labelFlag, + expiryFlag, + mailboxServerAddrFlag, + devserver, + cli.StringSliceFlag{ + Name: "feature", + Required: true, + }, + cli.StringFlag{ + Name: "channel-restrict-list", + Usage: "list of channel IDs that the " + + "autopilot server should not " + + "perform actions on. In the " + + "form of: chanID1,chanID2,...", + }, + cli.StringFlag{ + Name: "peer-restrict-list", + Usage: "list of peer IDs that the " + + "autopilot server should not " + + "perform actions on. In the " + + "form of: peerID1,peerID2,...", + }, + }, + }, + { + Name: "revoke", + ShortName: "r", + Usage: "revoke an autopilot session", + Description: "Revoke an active autopilot session", + Action: revokeAutopilotSession, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "localpubkey", + Usage: "local pubkey of the " + + "session to revoke", + Required: true, + }, + }, + }, + }, +} + +func revokeAutopilotSession(ctx *cli.Context) error { + ctxb := context.Background() + clientConn, cleanup, err := connectClient(ctx) + if err != nil { + return err + } + defer cleanup() + client := litrpc.NewAutopilotClient(clientConn) + + pubkey, err := hex.DecodeString(ctx.String("localpubkey")) + if err != nil { + return err + } + + resp, err := client.RevokeAutopilotSession( + ctxb, &litrpc.RevokeAutopilotSessionRequest{ + LocalPublicKey: pubkey, + }, + ) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} + +func listFeatures(ctx *cli.Context) error { + ctxb := context.Background() + clientConn, cleanup, err := connectClient(ctx) + if err != nil { + return err + } + defer cleanup() + client := litrpc.NewAutopilotClient(clientConn) + + resp, err := client.ListAutopilotFeatures( + ctxb, &litrpc.ListAutopilotFeaturesRequest{}, + ) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} + +func initAutopilotSession(ctx *cli.Context) error { + sessionLength := time.Second * time.Duration(ctx.Uint64("expiry")) + sessionExpiry := time.Now().Add(sessionLength).Unix() + + ctxb := context.Background() + clientConn, cleanup, err := connectClient(ctx) + if err != nil { + return err + } + defer cleanup() + client := litrpc.NewAutopilotClient(clientConn) + + ruleMap := &litrpc.RulesMap{ + Rules: make(map[string]*litrpc.RuleValue), + } + + chanRestrictList := ctx.String("channel-restrict-list") + if chanRestrictList != "" { + var chanIDs []uint64 + chans := strings.Split(chanRestrictList, ",") + for _, c := range chans { + i, err := strconv.ParseUint(c, 10, 64) + if err != nil { + return err + } + chanIDs = append(chanIDs, i) + } + + ruleMap.Rules[rules.ChannelRestrictName] = &litrpc.RuleValue{ + Value: &litrpc.RuleValue_ChannelRestrict{ + ChannelRestrict: &litrpc.ChannelRestrict{ + ChannelIds: chanIDs, + }, + }, + } + } + + peerRestrictList := ctx.String("peer-restrict-list") + if peerRestrictList != "" { + peerIDs := strings.Split(peerRestrictList, ",") + + ruleMap.Rules[rules.PeersRestrictName] = &litrpc.RuleValue{ + Value: &litrpc.RuleValue_PeerRestrict{ + PeerRestrict: &litrpc.PeerRestrict{ + PeerIds: peerIDs, + }, + }, + } + } + + featureMap := make(map[string]*litrpc.FeatureConfig) + for _, feature := range ctx.StringSlice("feature") { + featureMap[feature] = &litrpc.FeatureConfig{ + Rules: ruleMap, + Config: nil, + } + } + + resp, err := client.AddAutopilotSession( + ctxb, &litrpc.AddAutopilotSessionRequest{ + Label: ctx.String("label"), + ExpiryTimestampSeconds: uint64(sessionExpiry), + MailboxServerAddr: ctx.String("mailboxserveraddr"), + DevServer: ctx.Bool("devserver"), + Features: featureMap, + }, + ) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} diff --git a/cmd/litcli/main.go b/cmd/litcli/main.go index 72b339a87..4ccf03ed4 100644 --- a/cmd/litcli/main.go +++ b/cmd/litcli/main.go @@ -85,6 +85,9 @@ func main() { } app.Commands = append(app.Commands, sessionCommands...) app.Commands = append(app.Commands, accountsCommands...) + app.Commands = append(app.Commands, listActionsCommand) + app.Commands = append(app.Commands, privacyMapCommands) + app.Commands = append(app.Commands, autopilotCommands) err := app.Run(os.Args) if err != nil { diff --git a/cmd/litcli/privacy_map.go b/cmd/litcli/privacy_map.go new file mode 100644 index 000000000..e080bd371 --- /dev/null +++ b/cmd/litcli/privacy_map.go @@ -0,0 +1,134 @@ +package main + +import ( + "context" + "encoding/hex" + "fmt" + + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lightning-terminal/litrpc" + "github.com/urfave/cli" +) + +var privacyMapCommands = cli.Command{ + Name: "privacy", + ShortName: "p", + Usage: "Access the real-pseudo string pairs of the " + + "privacy mapper", + Category: "Privacy", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "session_id", + Usage: "The id of the session in question", + Required: true, + }, + cli.BoolFlag{ + Name: "realtopseudo", + Usage: "set to true if the input should be " + + "mapped to its pseudo counterpart. " + + "Otherwise the input will be taken " + + "as the pseudo value that should be " + + "mapped to its real counterpart.", + }, + }, + Subcommands: []cli.Command{ + privacyMapConvertStrCommand, + privacyMapConvertUint64Command, + }, +} + +var privacyMapConvertStrCommand = cli.Command{ + Name: "str", + ShortName: "s", + Usage: "convert a string to its real or pseudo counter part", + Action: privacyMapConvertStr, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "input", + Usage: "the string to convert", + Required: true, + }, + }, +} + +func privacyMapConvertStr(ctx *cli.Context) error { + ctxb := context.Background() + clientConn, cleanup, err := connectClient(ctx) + if err != nil { + return err + } + defer cleanup() + client := litrpc.NewFirewallClient(clientConn) + + id, err := hex.DecodeString(ctx.GlobalString("session_id")) + if err != nil { + return err + } + + resp, err := client.PrivacyMapConversion( + ctxb, &litrpc.PrivacyMapConversionRequest{ + SessionId: id, + RealToPseudo: ctx.GlobalBool("realtopseudo"), + Input: ctx.String("input"), + }, + ) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} + +var privacyMapConvertUint64Command = cli.Command{ + Name: "uint64", + ShortName: "u", + Usage: "convert a uint64 to its real or pseudo counter part", + Action: privacyMapConvertUint64, + Flags: []cli.Flag{ + cli.Uint64Flag{ + Name: "input", + Usage: "the uint64 to convert", + Required: true, + }, + }, +} + +func privacyMapConvertUint64(ctx *cli.Context) error { + ctxb := context.Background() + clientConn, cleanup, err := connectClient(ctx) + if err != nil { + return err + } + defer cleanup() + client := litrpc.NewFirewallClient(clientConn) + + id, err := hex.DecodeString(ctx.GlobalString("session_id")) + if err != nil { + return err + } + + input := firewalldb.Uint64ToStr(ctx.Uint64("input")) + + resp, err := client.PrivacyMapConversion( + ctxb, &litrpc.PrivacyMapConversionRequest{ + SessionId: id, + RealToPseudo: ctx.GlobalBool("realtopseudo"), + Input: input, + }, + ) + if err != nil { + return err + } + + output, err := firewalldb.StrToUint64(resp.Output) + if err != nil { + return err + } + + printRespJSON(&litrpc.PrivacyMapConversionResponse{ + Output: fmt.Sprintf("%d", output), + }) + return nil +} diff --git a/cmd/litcli/sessions.go b/cmd/litcli/sessions.go index 595919327..3a18fb4e0 100644 --- a/cmd/litcli/sessions.go +++ b/cmd/litcli/sessions.go @@ -15,6 +15,28 @@ var ( // defaultSessionExpiry is the default time a session can be used for. // The current value evaluates to 90 days. defaultSessionExpiry = time.Hour * 24 * 90 + + labelFlag = cli.StringFlag{ + Name: "label", + Usage: "session label", + Required: true, + } + expiryFlag = cli.Uint64Flag{ + Name: "expiry", + Usage: "number of seconds that the session should " + + "remain active", + Value: uint64(defaultSessionExpiry.Seconds()), + } + mailboxServerAddrFlag = cli.StringFlag{ + Name: "mailboxserveraddr", + Usage: "the host:port of the mailbox server to be used", + Value: "mailbox.terminal.lightning.today:443", + } + devserver = cli.BoolFlag{ + Name: "devserver", + Usage: "set to true to skip verification of the " + + "server's tls cert.", + } ) var sessionCommands = []cli.Command{ @@ -38,32 +60,16 @@ var addSessionCommand = cli.Command{ Description: "Add a new active session.", Action: addSession, Flags: []cli.Flag{ - cli.StringFlag{ - Name: "label", - Usage: "session label", - }, - cli.Uint64Flag{ - Name: "expiry", - Usage: "number of seconds that the session should " + - "remain active", - Value: uint64(defaultSessionExpiry.Seconds()), - }, - cli.StringFlag{ - Name: "mailboxserveraddr", - Usage: "the host:port of the mailbox server to be used", - Value: "mailbox.terminal.lightning.today:443", - }, - cli.BoolFlag{ - Name: "devserver", - Usage: "set to true to skip verification of the " + - "server's tls cert.", - }, + labelFlag, + expiryFlag, + mailboxServerAddrFlag, + devserver, cli.StringFlag{ Name: "type", Usage: "session type to be created which will " + "determine the permissions a user has when " + "connecting with the session. Options " + - "include readonly|admin|custom", + "include readonly|admin|account|custom", Value: "readonly", }, cli.StringSliceFlag{ @@ -79,6 +85,13 @@ var addSessionCommand = cli.Command{ "For example, '/lnrpc\\..*' will result in " + "all `lnrpc` permissions being included.", }, + cli.StringFlag{ + Name: "account_id", + Usage: "The account id that should be used for " + + "the account session. Note that this flag " + + "will only be used if the 'type' flag is " + + "set to 'account'.", + }, }, } @@ -90,11 +103,6 @@ func addSession(ctx *cli.Context) error { defer cleanup() client := litrpc.NewSessionsClient(clientConn) - label := ctx.String("label") - if label == "" { - return fmt.Errorf("must set a label for the session") - } - sessTypeStr := ctx.String("type") sessType, err := parseSessionType(sessTypeStr) if err != nil { @@ -115,12 +123,13 @@ func addSession(ctx *cli.Context) error { ctxb := context.Background() resp, err := client.AddSession( ctxb, &litrpc.AddSessionRequest{ - Label: label, + Label: ctx.String("label"), SessionType: sessType, ExpiryTimestampSeconds: uint64(sessionExpiry), MailboxServerAddr: ctx.String("mailboxserveraddr"), DevServer: ctx.Bool("devserver"), MacaroonCustomPermissions: macPerms, + AccountId: ctx.String("account_id"), }, ) if err != nil { @@ -138,6 +147,8 @@ func parseSessionType(sessionType string) (litrpc.SessionType, error) { return litrpc.SessionType_TYPE_MACAROON_ADMIN, nil case "readonly": return litrpc.SessionType_TYPE_MACAROON_READONLY, nil + case "account": + return litrpc.SessionType_TYPE_MACAROON_ACCOUNT, nil case "custom": return litrpc.SessionType_TYPE_MACAROON_CUSTOM, nil default: @@ -260,8 +271,9 @@ var revokeSessionCommand = cli.Command{ Action: revokeSession, Flags: []cli.Flag{ cli.StringFlag{ - Name: "localpubkey", - Usage: "local pubkey of the session to revoke", + Name: "localpubkey", + Usage: "local pubkey of the session to revoke", + Required: true, }, }, } diff --git a/config.go b/config.go index b99ab1d8b..cb9d741a7 100644 --- a/config.go +++ b/config.go @@ -17,6 +17,8 @@ import ( "github.com/lightninglabs/faraday" "github.com/lightninglabs/faraday/chain" "github.com/lightninglabs/faraday/frdrpcserver" + "github.com/lightninglabs/lightning-terminal/autopilotserver" + "github.com/lightninglabs/lightning-terminal/firewall" mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/loopd" @@ -185,6 +187,10 @@ type Config struct { RPCMiddleware *mid.Config `group:"RPC middleware options" namespace:"rpcmiddleware"` + Autopilot *autopilotserver.Config `group:"Autopilot server options" namespace:"autopilot"` + + Firewall *firewall.Config `group:"Firewall options" namespace:"firewall"` + // faradayRpcConfig is a subset of faraday's full configuration that is // passed into faraday's RPC server. faradayRpcConfig *frdrpcserver.Config @@ -325,6 +331,10 @@ func defaultConfig() *Config { Pool: &poolDefaultConfig, RPCMiddleware: mid.DefaultConfig(), FirstLNCConnDeadline: defaultFirstLNCConnTimeout, + Autopilot: &autopilotserver.Config{ + PingCadence: time.Hour, + }, + Firewall: firewall.DefaultConfig(), } } diff --git a/firewall/caveats.go b/firewall/caveats.go new file mode 100644 index 000000000..464a7ea26 --- /dev/null +++ b/firewall/caveats.go @@ -0,0 +1,173 @@ +package firewall + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/lightningnetwork/lnd/macaroons" + "gopkg.in/macaroon.v2" +) + +const ( + // RuleEnforcerCaveat is the name of the custom caveat that contains all + // the rules that need to be enforced in the firewall. + RuleEnforcerCaveat = "lit-mac-fw" + + // MetaInfoValuePrefix is the static prefix a macaroon caveat value has + // to mark the beginning of the meta information JSON data. + MetaInfoValuePrefix = "meta" + + // MetaRulesValuePrefix is the static prefix a macaroon caveat value has + // to mark the beginning of the rules list JSON data. + MetaRulesValuePrefix = "rules" + + // CondPrivacy is the name of the custom caveat that will + // instruct lnd to send all requests with this caveat to this + // interceptor. + CondPrivacy = "privacy" +) + +var ( + // MetaInfoFullCaveatPrefix is the full prefix a caveat needs to have to + // be recognized as a meta information caveat. + MetaInfoFullCaveatPrefix = fmt.Sprintf("%s %s %s", + macaroons.CondLndCustom, RuleEnforcerCaveat, + MetaInfoValuePrefix) + + // MetaRulesFullCaveatPrefix is the full prefix a caveat needs to have + // to be recognized as a rules list caveat. + MetaRulesFullCaveatPrefix = fmt.Sprintf("%s %s %s", + macaroons.CondLndCustom, RuleEnforcerCaveat, + MetaRulesValuePrefix) + + // MetaPrivacyCaveatPrefix is the caveat prefix that will be used to + // identify the privacy mapper caveat. + MetaPrivacyCaveatPrefix = fmt.Sprintf("%s %s", macaroons.CondLndCustom, + CondPrivacy) + + // MetaPrivacyCaveat is the caveat required to ensure that the + // privacy mapper is activated as an interceptor for a request. + MetaPrivacyCaveat = macaroon.Caveat{Id: []byte(MetaPrivacyCaveatPrefix)} + + // ErrNoMetaInfoCaveat is the error that is returned if a caveat doesn't + // have the prefix to be recognized as a meta information caveat. + ErrNoMetaInfoCaveat = fmt.Errorf("not a meta info caveat") + + // ErrNoRulesCaveat is the error that is returned if a caveat doesn't + // have the prefix to be recognized as a rules list caveat. + ErrNoRulesCaveat = fmt.Errorf("not a rules list caveat") +) + +// InterceptMetaInfo is the JSON serializable struct containing meta information +// about a request made by an automated node management software against LiT. +// The meta information is added as a macaroon caveat. +type InterceptMetaInfo struct { + // ActorName is the name of the actor service (=management software) + // that is issuing this request. + ActorName string `json:"actor_name"` + + // Feature is the feature that caused the actor to execute this action. + Feature string `json:"feature"` + + // Trigger is the action or condition that triggered this intercepted + // request to be made. + Trigger string `json:"trigger"` + + // Intent is the desired outcome or end condition this request aims to + // arrive at. + Intent string `json:"intent"` + + // StructuredJsonData is extra, structured, info that the Autopilot can + // send to Lit. It is a json serialised string. + StructuredJsonData string `json:"structured_json_data"` +} + +// ToCaveat returns the full custom caveat string representation of the +// interception meta information in this format: +// lnd-custom lit-mac-fw meta: +func (i *InterceptMetaInfo) ToCaveat() (string, error) { + jsonBytes, err := json.Marshal(i) + if err != nil { + return "", fmt.Errorf("error JSON marshaling: %v", err) + } + + return fmt.Sprintf("%s:%s", MetaInfoFullCaveatPrefix, jsonBytes), nil +} + +// ParseMetaInfoCaveat tries to parse the given caveat string as a meta +// information struct. +func ParseMetaInfoCaveat(caveat string) (*InterceptMetaInfo, error) { + if !strings.HasPrefix(caveat, MetaInfoFullCaveatPrefix) { + return nil, ErrNoMetaInfoCaveat + } + + // Only the prefix isn't enough. + if len(caveat) <= len(MetaInfoFullCaveatPrefix)+1 { + return nil, ErrNoMetaInfoCaveat + } + + // There's a colon after the prefix that we need to skip as well. + jsonData := caveat[len(MetaInfoFullCaveatPrefix)+1:] + i := &InterceptMetaInfo{} + + if err := json.Unmarshal([]byte(jsonData), i); err != nil { + return nil, fmt.Errorf("error unmarshaling JSON: %v", err) + } + + return i, nil +} + +// InterceptRules is the JSON serializable struct containing all the rules and +// their limits/settings that need to be enforced on a request made by an +// automated node management software against LiT. The rule information is added +// as a custom macaroon caveat. +type InterceptRules struct { + // SessionRules are rules that apply session wide. The map is rule + // name to rule value. + SessionRules map[string]string `json:"session_rules"` + + // Feature rules are rules that apply to a specific feature. The map is + // feature name to a map of rule name to rule value. + FeatureRules map[string]map[string]string `json:"feature_rules"` +} + +// RulesToCaveat encodes a list of rules as a full custom caveat string +// representation in this format: +// lnd-custom lit-mac-fw rules:[] +func RulesToCaveat(rules *InterceptRules) (string, error) { + jsonBytes, err := json.Marshal(rules) + if err != nil { + return "", fmt.Errorf("error JSON marshaling: %v", err) + } + + return fmt.Sprintf("%s:%s", MetaRulesFullCaveatPrefix, jsonBytes), nil +} + +// ParseRuleCaveat tries to parse the given caveat string as a rule struct. +func ParseRuleCaveat(caveat string) (*InterceptRules, error) { + if !strings.HasPrefix(caveat, MetaRulesFullCaveatPrefix) { + return nil, ErrNoRulesCaveat + } + + // Only the prefix isn't enough. + if len(caveat) <= len(MetaRulesFullCaveatPrefix)+1 { + return nil, ErrNoRulesCaveat + } + + // There's a colon after the prefix that we need to skip as well. + jsonData := caveat[len(MetaRulesFullCaveatPrefix)+1:] + var rules InterceptRules + + if err := json.Unmarshal([]byte(jsonData), &rules); err != nil { + return nil, fmt.Errorf("error unmarshaling JSON: %v", err) + } + + return &rules, nil +} + +// IsPrivacyCaveat returns true if the given caveat string is a privacy mapper +// caveat. +func IsPrivacyCaveat(caveat string) bool { + return strings.Contains(caveat, MetaPrivacyCaveatPrefix) +} diff --git a/firewall/caveats_test.go b/firewall/caveats_test.go new file mode 100644 index 000000000..9d8e4b81d --- /dev/null +++ b/firewall/caveats_test.go @@ -0,0 +1,183 @@ +package firewall + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + testMetaCaveat = "lnd-custom lit-mac-fw meta:{\"actor_name\":" + + "\"autopilot\",\"feature\":\"re-balance\",\"trigger\":" + + "\"channel 7413345453234435345 depleted\",\"intent\":" + + "\"increase outbound liquidity by 2000000 sats\"," + + "\"structured_json_data\":\"{}\"}" + + testRulesCaveat = "lnd-custom lit-mac-fw rules:{\"session_rules\":{" + + "\"rate-limit\":\"1/10\"},\"feature_rules\":{\"AutoFees\":{" + + "\"first-hop-ignore-list\":\"03abcd...,02badb01...\"," + + "\"max-hops\":\"4\"},\"Rebalance\":{\"off-chain-fees-sats\":" + + "\"10\",\"re-balance-min-interval-seconds\":\"3600\"}}}" +) + +// TestInterceptMetaInfo makes sure that a meta information struct can be +// formatted as a caveat and then parsed again successfully. +func TestInterceptMetaInfo(t *testing.T) { + info := &InterceptMetaInfo{ + ActorName: "autopilot", + Feature: "re-balance", + Trigger: "channel 7413345453234435345 depleted", + Intent: "increase outbound liquidity by 2000000 sats", + StructuredJsonData: "{}", + } + + caveat, err := info.ToCaveat() + require.NoError(t, err) + + require.Equal(t, testMetaCaveat, caveat) + + parsedInfo, err := ParseMetaInfoCaveat(caveat) + require.NoError(t, err) + + require.Equal(t, info, parsedInfo) +} + +// TestParseMetaInfoCaveat makes sure the meta information caveat parsing works +// as expected. +func TestParseMetaInfoCaveat(t *testing.T) { + testCases := []struct { + name string + input string + err error + result *InterceptMetaInfo + }{{ + name: "empty string", + input: "", + err: ErrNoMetaInfoCaveat, + }, { + name: "prefix only", + input: "lnd-custom lit-mac-fw meta:", + err: ErrNoMetaInfoCaveat, + }, { + name: "invalid JSON", + input: "lnd-custom lit-mac-fw meta:bar", + err: fmt.Errorf("error unmarshaling JSON: invalid character " + + "'b' looking for beginning of value"), + }, { + name: "empty JSON", + input: "lnd-custom lit-mac-fw meta:{}", + result: &InterceptMetaInfo{}, + }} + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(tt *testing.T) { + i, err := ParseMetaInfoCaveat(tc.input) + + if tc.err != nil { + require.Error(tt, err) + require.Equal(tt, tc.err, err) + + return + } + + require.NoError(tt, err) + require.Equal(tt, tc.result, i) + }) + } +} + +// TestInterceptRule makes sure that a rules list struct can be formatted as a +// caveat and then parsed again successfully. +func TestInterceptRule(t *testing.T) { + rules := &InterceptRules{ + FeatureRules: map[string]map[string]string{ + "AutoFees": { + "first-hop-ignore-list": "03abcd...,02badb01...", + "max-hops": "4", + }, + "Rebalance": { + "off-chain-fees-sats": "10", + "re-balance-min-interval-seconds": "3600", + }, + }, + SessionRules: map[string]string{ + "rate-limit": "1/10", + }, + } + + caveat, err := RulesToCaveat(rules) + require.NoError(t, err) + + require.Equal(t, testRulesCaveat, caveat) + + parsedRules, err := ParseRuleCaveat(caveat) + require.NoError(t, err) + + require.Equal(t, rules, parsedRules) +} + +// TestParseRulesCaveat makes sure the rule list caveat parsing works as +// expected. +func TestParseRulesCaveat(t *testing.T) { + testCases := []struct { + name string + input string + err error + result *InterceptRules + }{{ + name: "empty string", + input: "", + err: ErrNoRulesCaveat, + }, { + name: "prefix only", + input: "lnd-custom lit-mac-fw rules:", + err: ErrNoRulesCaveat, + }, { + name: "invalid JSON", + input: "lnd-custom lit-mac-fw rules:bar", + err: fmt.Errorf("error unmarshaling JSON: invalid character " + + "'b' looking for beginning of value"), + }, { + name: "empty JSON", + input: "lnd-custom lit-mac-fw rules:{}", + result: &InterceptRules{}, + }, { + name: "valid rules", + input: "lnd-custom lit-mac-fw rules:{\"session_rules\":" + + "{\"rate-limit\":\"2000\"}, \"feature_rules\":" + + "{\"Autofees\":{\"foo\":\"bar\", \"rate-limit\":" + + "\"1000\"}}}", + result: &InterceptRules{ + FeatureRules: map[string]map[string]string{ + "Autofees": { + "foo": "bar", + "rate-limit": "1000", + }, + }, + SessionRules: map[string]string{ + "rate-limit": "2000", + }, + }, + }} + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(tt *testing.T) { + i, err := ParseRuleCaveat(tc.input) + + if tc.err != nil { + require.Error(tt, err) + require.Equal(tt, tc.err, err) + + return + } + + require.NoError(tt, err) + require.Equal(tt, tc.result, i) + }) + } +} diff --git a/firewall/config.go b/firewall/config.go new file mode 100644 index 000000000..c0fdb758e --- /dev/null +++ b/firewall/config.go @@ -0,0 +1,20 @@ +package firewall + +// Config holds all config options for the firewall. +type Config struct { + RequestLogger *RequestLoggerConfig `group:"request-logger" namespace:"request-logger" description:"request logger settings"` +} + +// RequestLoggerConfig holds all the config options for the request logger. +type RequestLoggerConfig struct { + RequestLoggerLevel RequestLoggerLevel `long:"level" description:"Set the request logger level. Options include 'all', 'full' and 'interceptor''"` +} + +// DefaultConfig constructs the default firewall Config struct. +func DefaultConfig() *Config { + return &Config{ + RequestLogger: &RequestLoggerConfig{ + RequestLoggerLevel: RequestLoggerLevelInterceptor, + }, + } +} diff --git a/firewall/log.go b/firewall/log.go new file mode 100644 index 000000000..20bb3bed6 --- /dev/null +++ b/firewall/log.go @@ -0,0 +1,25 @@ +package firewall + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +const Subsystem = "FIRE" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/firewall/privacy_mapper.go b/firewall/privacy_mapper.go new file mode 100644 index 000000000..8bf55b174 --- /dev/null +++ b/firewall/privacy_mapper.go @@ -0,0 +1,835 @@ +package firewall + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "math/big" + "time" + + "github.com/lightninglabs/lightning-terminal/firewalldb" + mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" + "github.com/lightninglabs/lightning-terminal/session" + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/protobuf/proto" +) + +const ( + // privacyMapperName is the name of the RequestLogger interceptor. + privacyMapperName = "lit-privacy-mapper" + + // amountVariation and timeVariation are used to set the randomization + // of amounts and timestamps that are sent to the autopilot. Changing + // these values may lead to unintended consequences in the behavior of + // the autpilot. + amountVariation = 0.05 + timeVariation = time.Duration(10) * time.Minute + + // minTimeVariation and maxTimeVariation are the acceptable bounds + // between which timeVariation can be set. + minTimeVariation = time.Minute + maxTimeVariation = time.Duration(24) * time.Hour +) + +var ( + // ErrNotSupportedByPrivacyMapper indicates that the invoked RPC method + // is not supported by the privacy mapper. + ErrNotSupportedByPrivacyMapper = errors.New("this RPC call is not " + + "supported by the privacy mapper interceptor") +) + +// A compile-time assertion that PrivacyMapper is a +// rpcmiddleware.RequestInterceptor. +var _ mid.RequestInterceptor = (*PrivacyMapper)(nil) + +// PrivacyMapper is a RequestInterceptor that maps any pseudo names in certain +// requests to their real values and vice versa for responses. +type PrivacyMapper struct { + newDB firewalldb.NewPrivacyMapDB + randIntn func(int) (int, error) +} + +// NewPrivacyMapper returns a new instance of PrivacyMapper. The randIntn +// function is used to draw randomness for request field obfuscation. +func NewPrivacyMapper(newDB firewalldb.NewPrivacyMapDB, + randIntn func(int) (int, error)) *PrivacyMapper { + + return &PrivacyMapper{newDB: newDB, randIntn: randIntn} +} + +// Name returns the name of the interceptor. +func (p *PrivacyMapper) Name() string { + return privacyMapperName +} + +// ReadOnly returns true if this interceptor should be registered in read-only +// mode. In read-only mode no custom caveat name can be specified. +func (p *PrivacyMapper) ReadOnly() bool { + return false +} + +// CustomCaveatName returns the name of the custom caveat that is expected to be +// handled by this interceptor. Cannot be specified in read-only mode. +func (p *PrivacyMapper) CustomCaveatName() string { + return CondPrivacy +} + +// Intercept processes an RPC middleware interception request and returns the +// interception result which either accepts or rejects the intercepted message. +func (p *PrivacyMapper) Intercept(ctx context.Context, + req *lnrpc.RPCMiddlewareRequest) (*lnrpc.RPCMiddlewareResponse, error) { + + ri, err := NewInfoFromRequest(req) + if err != nil { + return nil, fmt.Errorf("error parsing incoming RPC middleware "+ + "interception request: %v", err) + } + + sessionID, err := session.IDFromMacaroon(ri.Macaroon) + if err != nil { + return nil, fmt.Errorf("could not extract ID from macaroon") + } + + log.Tracef("PrivacyMapper: Intercepting %v", ri) + + switch r := req.InterceptType.(type) { + case *lnrpc.RPCMiddlewareRequest_StreamAuth: + return mid.RPCErr(req, fmt.Errorf("streams unsupported")) + + // Parse incoming requests and act on them. + case *lnrpc.RPCMiddlewareRequest_Request: + msg, err := mid.ParseProtobuf( + r.Request.TypeName, r.Request.Serialized, + ) + if err != nil { + return mid.RPCErrString(req, "error parsing proto: %v", + err) + } + + replacement, err := p.checkAndReplaceIncomingRequest( + ctx, r.Request.MethodFullUri, msg, sessionID, + ) + if err != nil { + return mid.RPCErr(req, err) + } + + // No error occurred but the response should be replaced with + // the given custom response. Wrap it in the correct RPC + // response of the interceptor now. + if replacement != nil { + return mid.RPCReplacement(req, replacement) + } + + // No error and no replacement, just return an empty response of + // the correct type. + return mid.RPCOk(req) + + // Parse and possibly manipulate outgoing responses. + case *lnrpc.RPCMiddlewareRequest_Response: + if ri.IsError { + // TODO(elle): should we replace all litd errors with + // a generic error? + return mid.RPCOk(req) + } + + msg, err := mid.ParseProtobuf( + r.Response.TypeName, r.Response.Serialized, + ) + if err != nil { + return mid.RPCErrString(req, "error parsing proto: %v", + err) + } + + replacement, err := p.replaceOutgoingResponse( + ctx, r.Response.MethodFullUri, msg, sessionID, + ) + if err != nil { + return mid.RPCErr(req, err) + } + + // No error occurred but the response should be replaced with + // the given custom response. Wrap it in the correct RPC + // response of the interceptor now. + if replacement != nil { + return mid.RPCReplacement(req, replacement) + } + + // No error and no replacement, just return an empty response of + // the correct type. + return mid.RPCOk(req) + + default: + return mid.RPCErrString(req, "invalid intercept type: %v", r) + } +} + +// checkAndReplaceIncomingRequest inspects an incoming request and optionally +// modifies some of the request parameters. +func (p *PrivacyMapper) checkAndReplaceIncomingRequest(ctx context.Context, + uri string, req proto.Message, sessionID session.ID) (proto.Message, + error) { + + db := p.newDB(sessionID) + + // If we don't have a handler for the URI, we don't allow the request + // to go through. + checker, ok := p.checkers(db)[uri] + if !ok { + return nil, ErrNotSupportedByPrivacyMapper + } + + // This is just a sanity check to make sure the implementation for the + // checker actually matches the correct request type. + if !checker.HandlesRequest(req.ProtoReflect().Type()) { + return nil, fmt.Errorf("invalid implementation, checker for "+ + "URI %s does not accept request of type %v", uri, + req.ProtoReflect().Type()) + } + + return checker.HandleRequest(ctx, req) +} + +// replaceOutgoingResponse inspects the responses before sending them out to the +// client and replaces them if needed. +func (p *PrivacyMapper) replaceOutgoingResponse(ctx context.Context, uri string, + resp proto.Message, sessionID session.ID) (proto.Message, error) { + + db := p.newDB(sessionID) + + // If we don't have a handler for the URI, we don't allow the response + // to go to avoid accidental leaks. + checker, ok := p.checkers(db)[uri] + if !ok { + return nil, ErrNotSupportedByPrivacyMapper + } + + // This is just a sanity check to make sure the implementation for the + // checker actually matches the correct response type. + if !checker.HandlesResponse(resp.ProtoReflect().Type()) { + return nil, fmt.Errorf("invalid implementation, checker for "+ + "URI %s does not accept response of type %v", uri, + resp.ProtoReflect().Type()) + } + + return checker.HandleResponse(ctx, resp) +} + +func (p *PrivacyMapper) checkers( + db firewalldb.PrivacyMapDB) map[string]mid.RoundTripChecker { + + return map[string]mid.RoundTripChecker{ + "/lnrpc.Lightning/GetInfo": mid.NewResponseRewriter( + &lnrpc.GetInfoRequest{}, &lnrpc.GetInfoResponse{}, + handleGetInfoResponse(db), mid.PassThroughErrorHandler, + ), + "/lnrpc.Lightning/ForwardingHistory": mid.NewResponseRewriter( + &lnrpc.ForwardingHistoryRequest{}, + &lnrpc.ForwardingHistoryResponse{}, + handleFwdHistoryResponse(db, p.randIntn), + mid.PassThroughErrorHandler, + ), + "/lnrpc.Lightning/FeeReport": mid.NewResponseRewriter( + &lnrpc.FeeReportRequest{}, &lnrpc.FeeReportResponse{}, + handleFeeReportResponse(db), + mid.PassThroughErrorHandler, + ), + "/lnrpc.Lightning/ListChannels": mid.NewFullRewriter( + &lnrpc.ListChannelsRequest{}, + &lnrpc.ListChannelsResponse{}, + handleListChannelsRequest(db), + handleListChannelsResponse(db, p.randIntn), + mid.PassThroughErrorHandler, + ), + "/lnrpc.Lightning/UpdateChannelPolicy": mid.NewFullRewriter( + &lnrpc.PolicyUpdateRequest{}, + &lnrpc.PolicyUpdateResponse{}, + handleUpdatePolicyRequest(db), + handleUpdatePolicyResponse(db), + mid.PassThroughErrorHandler, + ), + } +} + +func handleGetInfoResponse(db firewalldb.PrivacyMapDB) func(ctx context.Context, + r *lnrpc.GetInfoResponse) (proto.Message, error) { + + return func(ctx context.Context, r *lnrpc.GetInfoResponse) ( + proto.Message, error) { + + var pseudoPubKey string + err := db.Update( + func(tx firewalldb.PrivacyMapTx) error { + var err error + pseudoPubKey, err = firewalldb.HideString( + tx, r.IdentityPubkey, + ) + if err != nil { + return err + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return &lnrpc.GetInfoResponse{ + // We purposefully hide our alias and URIs from the + // autopilot server. + Alias: "", + Color: "", + Uris: nil, + Version: r.Version, + CommitHash: r.CommitHash, + IdentityPubkey: pseudoPubKey, + NumPendingChannels: r.NumPendingChannels, + NumActiveChannels: r.NumActiveChannels, + NumInactiveChannels: r.NumInactiveChannels, + NumPeers: r.NumPeers, + BlockHeight: r.BlockHeight, + BlockHash: r.BlockHash, + BestHeaderTimestamp: r.BestHeaderTimestamp, + SyncedToChain: r.SyncedToChain, + SyncedToGraph: r.SyncedToGraph, + Testnet: r.Testnet, + Chains: r.Chains, + Features: r.Features, + RequireHtlcInterceptor: r.RequireHtlcInterceptor, + }, nil + } +} + +func handleFwdHistoryResponse(db firewalldb.PrivacyMapDB, + randIntn func(int) (int, error)) func(ctx context.Context, + r *lnrpc.ForwardingHistoryResponse) (proto.Message, error) { + + return func(_ context.Context, r *lnrpc.ForwardingHistoryResponse) ( + proto.Message, error) { + + fwdEvents := make( + []*lnrpc.ForwardingEvent, len(r.ForwardingEvents), + ) + + err := db.Update(func(tx firewalldb.PrivacyMapTx) error { + for i, fe := range r.ForwardingEvents { + // Deterministically hide channel ids. + chanIn, err := firewalldb.HideUint64( + tx, fe.ChanIdIn, + ) + if err != nil { + return err + } + + chanOut, err := firewalldb.HideUint64( + tx, fe.ChanIdOut, + ) + if err != nil { + return err + } + + // We randomize the outgoing amount for privacy. + amtOutMsat, err := hideAmount( + randIntn, amountVariation, + fe.AmtOutMsat, + ) + if err != nil { + return err + } + + // We randomize fees for privacy. + feeMsat, err := hideAmount( + randIntn, amountVariation, fe.FeeMsat, + ) + if err != nil { + return err + } + + // Populate other fields in a consistent manner. + amtInMsat := amtOutMsat + feeMsat + amtOut := amtOutMsat / 1000 + amtIn := amtInMsat / 1000 + fee := feeMsat / 1000 + + // We randomize the forwarding timestamp. + timestamp, err := hideTimestamp( + randIntn, timeVariation, + time.Unix(0, int64(fe.TimestampNs)), + ) + if err != nil { + return err + } + + fwdEvents[i] = &lnrpc.ForwardingEvent{ + ChanIdIn: chanIn, + ChanIdOut: chanOut, + AmtIn: amtIn, + AmtOut: amtOut, + Fee: fee, + FeeMsat: feeMsat, + AmtInMsat: amtInMsat, + AmtOutMsat: amtOutMsat, + TimestampNs: uint64( + timestamp.UnixNano(), + ), + Timestamp: uint64( + timestamp.Unix(), + ), + } + } + return nil + }) + if err != nil { + return nil, err + } + + return &lnrpc.ForwardingHistoryResponse{ + ForwardingEvents: fwdEvents, + LastOffsetIndex: r.LastOffsetIndex, + }, nil + } +} + +func handleFeeReportResponse(db firewalldb.PrivacyMapDB) func( + ctx context.Context, r *lnrpc.FeeReportResponse) (proto.Message, + error) { + + return func(ctx context.Context, r *lnrpc.FeeReportResponse) ( + proto.Message, error) { + + chanFees := make([]*lnrpc.ChannelFeeReport, len(r.ChannelFees)) + + err := db.Update(func(tx firewalldb.PrivacyMapTx) error { + for i, c := range r.ChannelFees { + chanID, err := firewalldb.HideUint64( + tx, c.ChanId, + ) + if err != nil { + return err + } + + chanPoint, err := firewalldb.HideChanPointStr( + tx, c.ChannelPoint, + ) + if err != nil { + return err + } + + chanFees[i] = &lnrpc.ChannelFeeReport{ + ChanId: chanID, + ChannelPoint: chanPoint, + BaseFeeMsat: c.BaseFeeMsat, + FeePerMil: c.FeePerMil, + FeeRate: c.FeeRate, + } + } + + return nil + }) + if err != nil { + return nil, err + } + + return &lnrpc.FeeReportResponse{ + ChannelFees: chanFees, + DayFeeSum: r.DayFeeSum, + WeekFeeSum: r.WeekFeeSum, + MonthFeeSum: r.MonthFeeSum, + }, nil + } +} + +func handleListChannelsRequest(db firewalldb.PrivacyMapDB) func( + ctx context.Context, r *lnrpc.ListChannelsRequest) (proto.Message, + error) { + + return func(ctx context.Context, r *lnrpc.ListChannelsRequest) ( + proto.Message, error) { + + if len(r.Peer) == 0 { + return nil, nil + } + + err := db.View(func(tx firewalldb.PrivacyMapTx) error { + peer, err := firewalldb.RevealBytes(tx, r.Peer) + if err != nil { + return err + } + + r.Peer = peer + return nil + }) + if err != nil { + return nil, err + } + + return r, nil + } +} + +func handleListChannelsResponse(db firewalldb.PrivacyMapDB, + randIntn func(int) (int, error)) func(ctx context.Context, + r *lnrpc.ListChannelsResponse) (proto.Message, error) { + + return func(_ context.Context, r *lnrpc.ListChannelsResponse) ( + proto.Message, error) { + + hideAmount := func(a int64) (int64, error) { + hiddenAmount, err := hideAmount( + randIntn, amountVariation, uint64(a), + ) + if err != nil { + return 0, err + } + + return int64(hiddenAmount), nil + } + + channels := make([]*lnrpc.Channel, len(r.Channels)) + + err := db.Update(func(tx firewalldb.PrivacyMapTx) error { + for i, c := range r.Channels { + // Deterministically hide the peer pubkey, + // the channel point, and the channel id. + remotePub, err := firewalldb.HideString( + tx, c.RemotePubkey, + ) + if err != nil { + return err + } + + chanPoint, err := firewalldb.HideChanPointStr( + tx, c.ChannelPoint, + ) + if err != nil { + return err + } + + chanID, err := firewalldb.HideUint64( + tx, c.ChanId, + ) + if err != nil { + return err + } + + // We hide the initiator. + initiator, err := hideBool(randIntn) + if err != nil { + return err + } + + // Consider the capacity to be public + // information. We don't care about reserves, as + // having some funds as a balance is the normal + // state over the lifetime of a channel. The + // balance would be zero only for the initial + // state as a non-funder. + + // We randomize local/remote balances. + localBalance, err := hideAmount(c.LocalBalance) + if err != nil { + return err + } + + // We may have a too large value for the local + // balance, restrict it to the capacity. + if localBalance > c.Capacity { + localBalance = c.Capacity + } + + // We adapt the remote balance accordingly. + remoteBalance := c.Capacity - localBalance + + // We hide the total sats sent and received. + satsReceived, err := hideAmount( + c.TotalSatoshisReceived, + ) + if err != nil { + return err + } + + satsSent, err := hideAmount( + c.TotalSatoshisSent, + ) + if err != nil { + return err + } + + // We only keep track of the _number_ of + // unsettled HTLCs. + pendingHtlcs := make( + []*lnrpc.HTLC, len(c.PendingHtlcs), + ) + + // We hide the unsettled balance. + unsettled, err := hideAmount(c.UnsettledBalance) + if err != nil { + return err + } + + //nolint:lll + channels[i] = &lnrpc.Channel{ + // Items we adjust. + RemotePubkey: remotePub, + ChannelPoint: chanPoint, + ChanId: chanID, + Initiator: initiator, + LocalBalance: localBalance, + RemoteBalance: remoteBalance, + TotalSatoshisReceived: satsReceived, + TotalSatoshisSent: satsSent, + UnsettledBalance: unsettled, + PendingHtlcs: pendingHtlcs, + + // Items that we zero out. + CloseAddress: "", + PushAmountSat: 0, + AliasScids: nil, + ZeroConfConfirmedScid: 0, + + // Items we keep as is. + Active: c.Active, + Capacity: c.Capacity, + CommitFee: c.CommitFee, + CommitWeight: c.CommitWeight, + FeePerKw: c.FeePerKw, + NumUpdates: c.NumUpdates, + CsvDelay: c.CsvDelay, + Private: c.Private, + ChanStatusFlags: c.ChanStatusFlags, + LocalChanReserveSat: c.LocalChanReserveSat, + RemoteChanReserveSat: c.RemoteChanReserveSat, + StaticRemoteKey: c.StaticRemoteKey, + CommitmentType: c.CommitmentType, + Lifetime: c.Lifetime, + Uptime: c.Uptime, + ThawHeight: c.ThawHeight, + LocalConstraints: c.LocalConstraints, + RemoteConstraints: c.RemoteConstraints, + ZeroConf: c.ZeroConf, + } + } + + return nil + }) + if err != nil { + return nil, err + } + + return &lnrpc.ListChannelsResponse{ + Channels: channels, + }, nil + } +} + +func handleUpdatePolicyRequest(db firewalldb.PrivacyMapDB) func( + ctx context.Context, r *lnrpc.PolicyUpdateRequest) (proto.Message, + error) { + + return func(ctx context.Context, r *lnrpc.PolicyUpdateRequest) ( + proto.Message, error) { + + chanPoint := r.GetChanPoint() + + // If no channel point is specified then the + // update request applies globally. + if chanPoint == nil { + return nil, nil + } + + txid, err := lnrpc.GetChanPointFundingTxid(chanPoint) + if err != nil { + return nil, err + } + + index := chanPoint.GetOutputIndex() + + var ( + newTxid string + newIndex uint32 + ) + err = db.View(func(tx firewalldb.PrivacyMapTx) error { + var err error + newTxid, newIndex, err = firewalldb.RevealChanPoint( + tx, txid.String(), index, + ) + return err + }) + if err != nil { + return nil, err + } + + r.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: newTxid, + }, + OutputIndex: newIndex, + }, + } + + return r, nil + } +} + +func handleUpdatePolicyResponse(db firewalldb.PrivacyMapDB) func( + ctx context.Context, r *lnrpc.PolicyUpdateResponse) (proto.Message, + error) { + + return func(ctx context.Context, r *lnrpc.PolicyUpdateResponse) ( + proto.Message, error) { + + failedUpdates := make( + []*lnrpc.FailedUpdate, len(r.FailedUpdates), + ) + + err := db.Update(func(tx firewalldb.PrivacyMapTx) error { + for i, u := range r.FailedUpdates { + failedUpdates[i] = &lnrpc.FailedUpdate{ + Reason: u.Reason, + UpdateError: u.UpdateError, + } + + if u.Outpoint == nil { + continue + } + + txid, index, err := firewalldb.HideChanPoint( + tx, u.Outpoint.TxidStr, + u.Outpoint.OutputIndex, + ) + if err != nil { + return err + } + + failedUpdates[i].Outpoint = &lnrpc.OutPoint{ + TxidBytes: nil, + TxidStr: txid, + OutputIndex: index, + } + } + + return nil + }) + if err != nil { + return nil, err + } + + return &lnrpc.PolicyUpdateResponse{ + FailedUpdates: failedUpdates, + }, nil + } +} + +// hideAmount symmetrically randomizes an amount around a given relative +// variation interval. relativeVariation should be between 0 and 1. +func hideAmount(randIntn func(n int) (int, error), relativeVariation float64, + amount uint64) (uint64, error) { + + if relativeVariation < 0 || relativeVariation > 1 { + return 0, fmt.Errorf("hide amount: relative variation is not "+ + "between allowed bounds of [0, 1], is %v", + relativeVariation) + } + + if amount == 0 { + return 0, nil + } + + // fuzzInterval is smaller than the amount provided fuzzVariation is + // between 0 and 1. + fuzzInterval := uint64(float64(amount) * relativeVariation) + + amountMin := int(amount - fuzzInterval) + amountMax := int(amount + fuzzInterval) + + randAmount, err := randBetween(randIntn, amountMin, amountMax) + if err != nil { + return 0, err + } + + return uint64(randAmount), nil +} + +// hideTimestamp symmetrically randomizes a unix timestamp given an absolute +// variation interval. The random input is expected to be rand.Intn. +func hideTimestamp(randIntn func(n int) (int, error), + absoluteVariation time.Duration, + timestamp time.Time) (time.Time, error) { + + if absoluteVariation < minTimeVariation || + absoluteVariation > maxTimeVariation { + + return time.Time{}, fmt.Errorf("hide timestamp: absolute time "+ + "variation is out of bounds, have %v", + absoluteVariation) + } + + // Don't fuzz meaningless timestamps. + if timestamp.Add(-absoluteVariation).Unix() < 0 || + timestamp.IsZero() { + + return timestamp, nil + } + + // We vary symmetrically around the provided timestamp. + timeMin := timestamp.Add(-absoluteVariation) + timeMax := timestamp.Add(absoluteVariation) + + timeNs, err := randBetween( + randIntn, int(timeMin.UnixNano()), int(timeMax.UnixNano()), + ) + if err != nil { + return time.Time{}, err + } + + return time.Unix(0, int64(timeNs)), nil +} + +// randBetween generates a random number between [min, max) given a source of +// randomness. +func randBetween(randIntn func(int) (int, error), min, max int) (int, error) { + if max < min { + return 0, fmt.Errorf("min is not allowed to be greater than "+ + "max, (min: %v, max: %v)", min, max) + } + + // We don't want to pass zero to randIntn to avoid panics. + if max == min { + return min, nil + } + + add, err := randIntn(max - min) + if err != nil { + return 0, err + } + + return min + add, nil +} + +// hideBool generates a random bool given a random input. +func hideBool(randIntn func(n int) (int, error)) (bool, error) { + random, err := randIntn(2) + if err != nil { + return false, err + } + + // For testing we may expect larger random numbers, which we map to + // true. + return random >= 1, nil +} + +// CryptoRandIntn generates a random number between [0, n). +func CryptoRandIntn(n int) (int, error) { + if n == 0 { + return 0, nil + } + + nBig, err := rand.Int(rand.Reader, big.NewInt(int64(n))) + if err != nil { + return 0, err + } + + return int(nBig.Int64()), nil +} diff --git a/firewall/privacy_mapper_test.go b/firewall/privacy_mapper_test.go new file mode 100644 index 000000000..3778363b6 --- /dev/null +++ b/firewall/privacy_mapper_test.go @@ -0,0 +1,702 @@ +package firewall + +import ( + "context" + "testing" + "time" + + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lightning-terminal/session" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/rpcperms" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon.v2" +) + +// TestPrivacyMapper tests that the PrivacyMapper correctly intercepts specific +// RPC calls. +func TestPrivacyMapper(t *testing.T) { + tests := []struct { + name string + uri string + msgType rpcperms.InterceptType + msg proto.Message + expectedReplacement proto.Message + }{ + { + name: "GetInfo Response", + uri: "/lnrpc.Lightning/GetInfo", + msgType: rpcperms.TypeResponse, + msg: &lnrpc.GetInfoResponse{ + Alias: "Tinker Bell", + IdentityPubkey: "Tinker Bell's pub key", + Uris: []string{ + "Neverland 1", + "Neverland 2", + }, + }, + expectedReplacement: &lnrpc.GetInfoResponse{ + IdentityPubkey: "a44ef01c3bff970ef495c", + }, + }, + { + name: "ForwardingHistory Response", + uri: "/lnrpc.Lightning/ForwardingHistory", + msgType: rpcperms.TypeResponse, + msg: &lnrpc.ForwardingHistoryResponse{ + ForwardingEvents: []*lnrpc.ForwardingEvent{ + { + AmtIn: 2_000, + AmtInMsat: 2_000_000, + AmtOut: 1_000, + AmtOutMsat: 1_000_000, + Fee: 1_000, + FeeMsat: 1_000_000, + Timestamp: 1_000, + TimestampNs: 1_000_000_000_000, + ChanIdIn: 123, + ChanIdOut: 321, + }, + { + AmtIn: 3_000, + AmtInMsat: 3_000_000, + AmtOut: 2_000, + AmtOutMsat: 2_000_000, + Fee: 1_000, + FeeMsat: 1_000_000, + Timestamp: 1_000, + TimestampNs: 1_000_000_000_000, + ChanIdIn: 678, + ChanIdOut: 876, + }, + }, + }, + expectedReplacement: &lnrpc.ForwardingHistoryResponse{ + ForwardingEvents: []*lnrpc.ForwardingEvent{ + { + AmtIn: 1_900, + AmtInMsat: 1_900_200, + AmtOut: 950, + AmtOutMsat: 950_100, + Fee: 950, + FeeMsat: 950_100, + Timestamp: 400, + TimestampNs: 400_000_000_100, + ChanIdIn: 5178778334600911958, + ChanIdOut: 3446430762436373227, + }, + { + AmtIn: 2_850, + AmtInMsat: 2_850_200, + AmtOut: 1_900, + AmtOutMsat: 1_900_100, + Fee: 950, + FeeMsat: 950_100, + Timestamp: 400, + TimestampNs: 400_000_000_100, + ChanIdIn: 8672172843977902018, + ChanIdOut: 1378354177616075123, + }, + }, + }, + }, + { + name: "FeeReport Response", + uri: "/lnrpc.Lightning/FeeReport", + msgType: rpcperms.TypeResponse, + msg: &lnrpc.FeeReportResponse{ + ChannelFees: []*lnrpc.ChannelFeeReport{ + { + ChanId: 123, + ChannelPoint: "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd:0", + }, + { + ChanId: 321, + ChannelPoint: "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd:1", + }, + }, + }, + expectedReplacement: &lnrpc.FeeReportResponse{ + ChannelFees: []*lnrpc.ChannelFeeReport{ + { + ChanId: 5178778334600911958, + ChannelPoint: "097ef666a61919ff3413b3b701eae3a5cbac08f70c0ca567806e1fa6acbfe384:2161781494", + }, + { + ChanId: 3446430762436373227, + ChannelPoint: "45ec471bfccb0b7b9a8bc4008248931c59ad994903e07b54f54821ea3ef5cc5c62:1642614131", + }, + }, + }, + }, + { + name: "ListChannels Request", + uri: "/lnrpc.Lightning/ListChannels", + msgType: rpcperms.TypeRequest, + msg: &lnrpc.ListChannelsRequest{ + Peer: []byte{200, 19, 68, 149}, + }, + expectedReplacement: &lnrpc.ListChannelsRequest{ + Peer: []byte{1, 2, 3, 4}, + }, + }, + { + name: "ListChannels Response", + uri: "/lnrpc.Lightning/ListChannels", + msgType: rpcperms.TypeResponse, + msg: &lnrpc.ListChannelsResponse{ + Channels: []*lnrpc.Channel{ + { + Capacity: 1_000_000, + RemoteBalance: 600_000, + LocalBalance: 499_000, + CommitFee: 1_000, + TotalSatoshisSent: 500_000, + TotalSatoshisReceived: 450_000, + RemotePubkey: "01020304", + Initiator: false, + ChanId: 123, + ChannelPoint: "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd:0", + PendingHtlcs: []*lnrpc.HTLC{{HashLock: []byte("aaaa")}, {HashLock: []byte("bbbb")}}, + }, + }, + }, + expectedReplacement: &lnrpc.ListChannelsResponse{ + Channels: []*lnrpc.Channel{ + { + Capacity: 1_000_000, + RemoteBalance: 525_850, + LocalBalance: 474_150, + CommitFee: 1_000, + TotalSatoshisSent: 475_100, + TotalSatoshisReceived: 427_600, + RemotePubkey: "c8134495", + Initiator: true, + ChanId: 5178778334600911958, + ChannelPoint: "097ef666a61919ff3413b3b701eae3a5cbac08f70c0ca567806e1fa6acbfe384:2161781494", + PendingHtlcs: []*lnrpc.HTLC{{}, {}}, + }, + }, + }, + }, + { + name: "UpdateChannelPolicy Request txid string", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msgType: rpcperms.TypeRequest, + msg: &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: "097ef666a61919ff3413b3b701eae3a5cbac08f70c0ca567806e1fa6acbfe384", + }, + OutputIndex: 2161781494, + }, + }, + }, + expectedReplacement: &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd", + }, + OutputIndex: 0, + }, + }, + }, + }, + { + name: "UpdateChannelPolicy Request txid bytes", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msgType: rpcperms.TypeRequest, + msg: &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ + FundingTxidBytes: []byte{132, 227, 191, 172, 166, 31, 110, 128, 103, 165, 12, 12, 247, 8, 172, 203, 165, 227, 234, 1, 183, 179, 19, 52, 255, 25, 25, 166, 102, 246, 126, 9}, + }, + OutputIndex: 2161781494, + }, + }, + }, + expectedReplacement: &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd", + }, + OutputIndex: 0, + }, + }, + }, + }, + { + name: "UpdateChannelPolicy Response", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msgType: rpcperms.TypeResponse, + msg: &lnrpc.PolicyUpdateResponse{ + FailedUpdates: []*lnrpc.FailedUpdate{ + { + Outpoint: &lnrpc.OutPoint{ + TxidStr: "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd", + OutputIndex: 0, + }, + }, + }, + }, + expectedReplacement: &lnrpc.PolicyUpdateResponse{ + FailedUpdates: []*lnrpc.FailedUpdate{ + { + Outpoint: &lnrpc.OutPoint{ + TxidStr: "097ef666a61919ff3413b3b701eae3a5cbac08f70c0ca567806e1fa6acbfe384", + OutputIndex: 2161781494, + }, + }, + }, + }, + }, + } + + decodedID := &lnrpc.MacaroonId{ + StorageId: []byte("123"), + } + b, err := proto.Marshal(decodedID) + require.NoError(t, err) + + rawID := make([]byte, len(b)+1) + rawID[0] = byte(bakery.LatestVersion) + copy(rawID[1:], b) + + mac, err := macaroon.New( + []byte("123"), rawID, "", macaroon.V2, + ) + require.NoError(t, err) + + macBytes, err := mac.MarshalBinary() + require.NoError(t, err) + + sessionID, err := session.IDFromMacaroon(mac) + require.NoError(t, err) + + mapPreloadRealToPseudo := map[string]string{ + "Tinker Bell's pub key": "a44ef01c3bff970ef495c", + "000000000000007b": "47deb774fc605c56", + "0000000000000141": "2fd42e84b9ffaaeb", + "00000000000002a6": "7859bf41241787c2", + "000000000000036c": "1320e5d25b7b5973", + "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd:0": "097ef666a61919ff3413b3b701eae3a5cbac08f70c0ca567806e1fa6acbfe384:2161781494", + "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd:1": "45ec471bfccb0b7b9a8bc4008248931c59ad994903e07b54f54821ea3ef5cc5c62:1642614131", + "01020304": "c8134495", + } + + db := newMockDB(t, mapPreloadRealToPseudo, sessionID) + + // randIntn is used for deterministic testing. + randIntn := func(n int) (int, error) { return 100, nil } + p := NewPrivacyMapper(db.NewSessionDB, randIntn) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rawMsg, err := proto.Marshal(test.msg) + require.NoError(t, err) + + interceptReq := &rpcperms.InterceptionRequest{ + Type: test.msgType, + Macaroon: mac, + RawMacaroon: macBytes, + FullURI: test.uri, + ProtoSerialized: rawMsg, + ProtoTypeName: string( + proto.MessageName(test.msg), + ), + } + + mwReq, err := interceptReq.ToRPC(1, 2) + require.NoError(t, err) + + resp, err := p.Intercept(context.Background(), mwReq) + require.NoError(t, err) + + feedback := resp.GetFeedback() + if test.expectedReplacement == nil { + require.False(t, feedback.ReplaceResponse) + return + } + + expectedRaw, err := proto.Marshal( + test.expectedReplacement, + ) + require.NoError(t, err) + require.Equal( + t, expectedRaw, feedback.ReplacementSerialized, + ) + }) + } + + // Subtest to test behavior with real randomness. + t.Run("Response with randomness", func(t *testing.T) { + msg := &lnrpc.ForwardingHistoryResponse{ + ForwardingEvents: []*lnrpc.ForwardingEvent{ + { + AmtIn: 2_000, + AmtInMsat: 2_000_000, + AmtOut: 1_000, + AmtOutMsat: 1_000_000, + Fee: 0, + FeeMsat: 1, + Timestamp: 1_000_000, + TimestampNs: 1_000_000 * 1e9, + ChanIdIn: 123, + ChanIdOut: 321, + }, + }, + } + rawMsg, err := proto.Marshal(msg) + require.NoError(t, err) + + p = NewPrivacyMapper(db.NewSessionDB, CryptoRandIntn) + require.NoError(t, err) + + // We test the independent outgoing amount (incoming amount + // would also be dependend on the fee variation). + amtOutMsat := msg.ForwardingEvents[0].AmtOutMsat + amtInterval := uint64(amountVariation * float64(amtOutMsat)) + minAmt := amtOutMsat - amtInterval + maxAmt := amtOutMsat + amtInterval + + // We keep track of the timestamp. We test only the timestamp in + // seconds as there can be numerical inaccuracies with the + // nanosecond one. + timestamp := msg.ForwardingEvents[0].Timestamp + timestampInterval := uint64(timeVariation) / 1e9 + minTime := timestamp - timestampInterval + maxTime := timestamp + timestampInterval + + // We need a certain number of samples to have statistical + // accuracy. + numSamples := 10_000 + + // We require a five percent accuracy for 10_000 samples. + relativeTestAccuracy := 0.05 + + amounts := make([]uint64, numSamples) + timestamps := make([]uint64, numSamples) + + for i := 0; i < numSamples; i++ { + interceptReq := &rpcperms.InterceptionRequest{ + Type: rpcperms.TypeResponse, + Macaroon: mac, + RawMacaroon: macBytes, + FullURI: "/lnrpc.Lightning/ForwardingHistory", + ProtoSerialized: rawMsg, + ProtoTypeName: string( + proto.MessageName(msg), + ), + } + + mwReq, err := interceptReq.ToRPC(1, 2) + require.NoError(t, err) + + resp, err := p.Intercept(context.Background(), mwReq) + require.NoError(t, err) + + feedback := resp.GetFeedback() + + fw := &lnrpc.ForwardingHistoryResponse{} + err = proto.Unmarshal( + feedback.ReplacementSerialized, fw, + ) + require.NoError(t, err) + + amounts[i] = fw.ForwardingEvents[0].AmtOutMsat + require.LessOrEqual(t, amounts[i], maxAmt) + require.GreaterOrEqual(t, amounts[i], minAmt) + + timestamps[i] = fw.ForwardingEvents[0].Timestamp + require.LessOrEqual(t, timestamps[i], maxTime) + require.GreaterOrEqual(t, timestamps[i], minTime) + } + + // The formula for the expected variance is taken from + // https://en.wikipedia.org/wiki/Continuous_uniform_distribution + expectedVar := func(min, max uint64) uint64 { + return (max - min) * (max - min) / 12 + } + + // Test amounts for mean and variance. + expectedAmtVariance := expectedVar(minAmt, maxAmt) + require.InEpsilon(t, expectedAmtVariance, variance(amounts), + relativeTestAccuracy) + require.InEpsilon(t, amtOutMsat, mean(amounts), + relativeTestAccuracy) + + // Test timestamps for mean and variance. + expectedTimeVariance := expectedVar(minTime, maxTime) + require.InEpsilon(t, expectedTimeVariance, variance(timestamps), + relativeTestAccuracy) + require.InEpsilon(t, timestamp, mean(timestamps), + relativeTestAccuracy) + }) +} + +type mockDB map[string]*mockPrivacyMapDB + +func newMockDB(t *testing.T, preloadRealToPseudo map[string]string, + sessID session.ID) mockDB { + + db := make(mockDB) + sessDB := db.NewSessionDB(sessID) + + _ = sessDB.Update(func(tx firewalldb.PrivacyMapTx) error { + for r, p := range preloadRealToPseudo { + require.NoError(t, tx.NewPair(r, p)) + } + return nil + }) + + return db +} + +func (m mockDB) NewSessionDB(sessionID session.ID) firewalldb.PrivacyMapDB { + db, ok := m[string(sessionID[:])] + if ok { + return db + } + + newDB := newMockPrivacyMapDB() + m[string(sessionID[:])] = newDB + + return newDB +} + +func newMockPrivacyMapDB() *mockPrivacyMapDB { + return &mockPrivacyMapDB{ + r2p: make(map[string]string), + p2r: make(map[string]string), + } +} + +type mockPrivacyMapDB struct { + r2p map[string]string + p2r map[string]string +} + +func (m *mockPrivacyMapDB) Update( + f func(tx firewalldb.PrivacyMapTx) error) error { + + return f(m) +} + +func (m *mockPrivacyMapDB) View( + f func(tx firewalldb.PrivacyMapTx) error) error { + + return f(m) +} + +func (m *mockPrivacyMapDB) NewPair(real, pseudo string) error { + m.r2p[real] = pseudo + m.p2r[pseudo] = real + return nil +} + +func (m *mockPrivacyMapDB) PseudoToReal(pseudo string) (string, error) { + r, ok := m.p2r[pseudo] + if !ok { + return "", firewalldb.ErrNoSuchKeyFound + } + + return r, nil +} + +func (m *mockPrivacyMapDB) RealToPseudo(real string) (string, error) { + p, ok := m.r2p[real] + if !ok { + return "", firewalldb.ErrNoSuchKeyFound + } + + return p, nil +} + +var _ firewalldb.PrivacyMapDB = (*mockPrivacyMapDB)(nil) + +// TestRandBetween tests random number generation for numbers in an interval. +func TestRandBetween(t *testing.T) { + min := 0 + max := 10 + + for i := 0; i < 100; i++ { + val, err := randBetween(CryptoRandIntn, min, max) + require.NoError(t, err) + require.Less(t, val, max) + require.GreaterOrEqual(t, val, min) + } +} + +// TestHideAmount tests that we hide amounts correctly. +func TestHideAmount(t *testing.T) { + testAmount := uint64(10_000) + relativeVariation := 0.05 + absoluteVariation := int(float64(testAmount) * relativeVariation) + lowerBound := testAmount - uint64(absoluteVariation) + upperBound := testAmount + uint64(absoluteVariation) + + tests := []struct { + name string + amount uint64 + randIntFn func(int) (int, error) + expected uint64 + }{ + { + name: "zero test amount", + randIntFn: func(int) (int, error) { return 0, nil }, + }, + { + name: "test small amount", + randIntFn: func(int) (int, error) { return 0, nil }, + amount: 1, + expected: 1, + }, + { + name: "min value", + randIntFn: func(int) (int, error) { return 0, nil }, + amount: testAmount, + expected: lowerBound, + }, + { + name: "max value", + randIntFn: func(int) (int, error) { + return int(upperBound - lowerBound), nil + }, + amount: testAmount, + expected: upperBound, + }, + { + name: "some fuzz", + randIntFn: func(int) (int, error) { return 123, nil }, + amount: testAmount, + expected: lowerBound + 123, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + val, err := hideAmount( + test.randIntFn, + relativeVariation, + test.amount, + ) + require.NoError(t, err) + require.Equal(t, test.expected, val) + }) + } + + // Subtest with real randomness. + t.Run("real randomness for small numbers", func(t *testing.T) { + for i := 0; i < 1000; i++ { + _, err := hideAmount( + CryptoRandIntn, + relativeVariation, + uint64(i), + ) + require.NoError(t, err) + } + }) +} + +// TestHideTimestamp test correct timestamp hiding. +func TestHideTimestamp(t *testing.T) { + timestamp := time.Unix(1_000_000, 0) + absoluteVariation := time.Duration(10) * time.Minute + lowerBound := timestamp.Add(-absoluteVariation) + upperBound := timestamp.Add(absoluteVariation) + + tests := []struct { + name string + randIntFn func(int) (int, error) + timestamp time.Time + expected time.Time + }{ + { + name: "zero timestamp", + randIntFn: func(int) (int, error) { return 0, nil }, + }, + { + name: "min value", + randIntFn: func(int) (int, error) { return 0, nil }, + timestamp: timestamp, + expected: lowerBound, + }, + { + name: "max value", + randIntFn: func(int) (int, error) { + return int(upperBound.Sub(lowerBound)), nil + }, + timestamp: timestamp, + expected: upperBound, + }, + { + name: "some fuzz", + randIntFn: func(int) (int, error) { return 123, nil }, + timestamp: timestamp, + expected: lowerBound.Add(time.Duration(123)), + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + val, err := hideTimestamp( + test.randIntFn, + absoluteVariation, + test.timestamp, + ) + require.NoError(t, err) + require.Equal(t, test.expected, val) + }) + } +} + +// TestHideBool test correct boolean hiding. +func TestHideBool(t *testing.T) { + val, err := hideBool(func(int) (int, error) { return 100, nil }) + require.NoError(t, err) + require.True(t, val) + + val, err = hideBool(func(int) (int, error) { return 1, nil }) + require.NoError(t, err) + require.True(t, val) + + val, err = hideBool(func(int) (int, error) { return 0, nil }) + require.NoError(t, err) + require.False(t, val) +} + +// mean computes the mean of the given slice of numbers. +func mean(numbers []uint64) uint64 { + sum := uint64(0) + + for _, n := range numbers { + sum += n + } + + return sum / uint64(len(numbers)) +} + +// variance computes the variance of the given slice of numbers. +func variance(numbers []uint64) uint64 { + mean := mean(numbers) + sum := 0.0 + + // We divide in each step to have smaller numbers. + norm := float64(len(numbers) - 1) + + for _, n := range numbers { + sum += float64((n-mean)*(n-mean)) / norm + } + + return uint64(sum) +} diff --git a/firewall/request_info.go b/firewall/request_info.go new file mode 100644 index 000000000..10a524934 --- /dev/null +++ b/firewall/request_info.go @@ -0,0 +1,136 @@ +package firewall + +import ( + "fmt" + "strings" + + "github.com/lightningnetwork/lnd/lnrpc" + "gopkg.in/macaroon.v2" +) + +const ( + // MWRequestTypeStreamAuth represents the type name for a stream + // authentication interception message. + MWRequestTypeStreamAuth = "stream_auth" + + // MWRequestTypeRequest represents the type name for a request + // interception message. + MWRequestTypeRequest = "request" + + // MWRequestTypeResponse represents the type name for a response + // interception message. + MWRequestTypeResponse = "response" +) + +// RequestInfo stores the parsed representation of an incoming RPC middleware +// request. +type RequestInfo struct { + MsgID uint64 + RequestID uint64 + MWRequestType string + URI string + GRPCMessageType string + IsError bool + Serialized []byte + Streaming bool + Macaroon *macaroon.Macaroon + Caveats []string + MetaInfo *InterceptMetaInfo + Rules *InterceptRules + WithPrivacy bool +} + +// NewInfoFromRequest parses the given RPC middleware interception request and +// returns a RequestInfo struct. +func NewInfoFromRequest(req *lnrpc.RPCMiddlewareRequest) (*RequestInfo, error) { + var ri *RequestInfo + switch t := req.InterceptType.(type) { + case *lnrpc.RPCMiddlewareRequest_StreamAuth: + ri = &RequestInfo{ + MWRequestType: MWRequestTypeStreamAuth, + URI: t.StreamAuth.MethodFullUri, + Streaming: true, + } + + case *lnrpc.RPCMiddlewareRequest_Request: + ri = &RequestInfo{ + MWRequestType: MWRequestTypeRequest, + URI: t.Request.MethodFullUri, + GRPCMessageType: t.Request.TypeName, + IsError: t.Request.IsError, + Serialized: t.Request.Serialized, + Streaming: t.Request.StreamRpc, + } + + case *lnrpc.RPCMiddlewareRequest_Response: + ri = &RequestInfo{ + MWRequestType: MWRequestTypeResponse, + URI: t.Response.MethodFullUri, + GRPCMessageType: t.Response.TypeName, + IsError: t.Response.IsError, + Serialized: t.Response.Serialized, + Streaming: t.Response.StreamRpc, + } + + default: + return nil, fmt.Errorf("invalid request type: %T", t) + } + + ri.MsgID = req.MsgId + ri.RequestID = req.RequestId + + // If there is no macaroon in the request, then there is nothing left + // to parse. + if len(req.RawMacaroon) == 0 { + return ri, nil + } + + ri.Macaroon = &macaroon.Macaroon{} + if err := ri.Macaroon.UnmarshalBinary(req.RawMacaroon); err != nil { + return nil, fmt.Errorf("error parsing macaroon: %v", err) + } + + ri.Caveats = make([]string, len(ri.Macaroon.Caveats())) + for idx, cav := range ri.Macaroon.Caveats() { + ri.Caveats[idx] = string(cav.Id) + + // Apply any meta information sent as a custom caveat. Only the + // last one will be considered if there are multiple caveats. + metaInfo, err := ParseMetaInfoCaveat(ri.Caveats[idx]) + if err == nil { + ri.MetaInfo = metaInfo + + // The same caveat can't be a meta info and a rule list + // or a privacy caveat. + continue + } + + // Also apply the rule list sent as a custom caveat. Only the + // last set of rules will be considered if there are multiple + // caveats. + rules, err := ParseRuleCaveat(ri.Caveats[idx]) + if err == nil { + ri.Rules = rules + + // The same caveat can't be a rule list and a privacy + // caveat. + continue + } + + if IsPrivacyCaveat(ri.Caveats[idx]) { + ri.WithPrivacy = true + } + } + + return ri, nil +} + +// String returns the string representation of the request info struct. +func (ri *RequestInfo) String() string { + return fmt.Sprintf("Request={msg_id=%d, request_id=%d, type=%v, "+ + "uri=%v, grpc_message_type=%v, streaming=%v, caveats=[%v], "+ + "meta_info=%v, rules=[%v]}", + ri.MsgID, ri.RequestID, ri.MWRequestType, ri.URI, + ri.GRPCMessageType, ri.Streaming, strings.Join(ri.Caveats, ","), + ri.MetaInfo, ri.Rules) +} diff --git a/firewall/request_logger.go b/firewall/request_logger.go new file mode 100644 index 000000000..c3b6e61f8 --- /dev/null +++ b/firewall/request_logger.go @@ -0,0 +1,264 @@ +package firewall + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/golang/protobuf/proto" + "github.com/lightninglabs/lightning-terminal/firewalldb" + mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" + "github.com/lightninglabs/lightning-terminal/session" + "github.com/lightninglabs/protobuf-hex-display/jsonpb" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/macaroons" +) + +const ( + // RequestLoggerName is the name of the RequestLogger interceptor. + RequestLoggerName = "lit-macaroon-firewall-logger" +) + +var ( + // uriSkipList is a map of URIs that we don't want to log or persist + // actions for. + uriSkipList = map[string]bool{ + // We skip the CheckMacaroonPermissions uri since this method is + // called each time a call to a non-Litd endpoint needs to be + // validated and persisting the macaroon each time bloats the + // DB. + "/lnrpc.Lightning/CheckMacaroonPermissions": true, + } + + // A compile-time assertion that RuleEnforcer is a + // rpcmiddleware.RequestInterceptor. + _ mid.RequestInterceptor = (*RequestLogger)(nil) +) + +type RequestLoggerLevel string + +const ( + RequestLoggerLevelInterceptor = "interceptor" + RequestLoggerLevelAll = "all" + RequestLoggerLevelFull = "full" +) + +// RequestLogger is a RequestInterceptor that just logs incoming RPC requests. +type RequestLogger struct { + actionsDB firewalldb.ActionsWriteDB + + shouldLogAction func(ri *RequestInfo) (bool, bool) + + // reqIDToAction is a map from request ID to an ActionLocator that can + // be used to find the corresponding action. This is used so that + // requests and responses can be easily linked. The mu mutex must be + // used when accessing this map. + reqIDToAction map[uint64]*firewalldb.ActionLocator + mu sync.Mutex +} + +// NewRequestLogger creates a new RequestLogger. +func NewRequestLogger(cfg *RequestLoggerConfig, + actionsDB firewalldb.ActionsWriteDB) (*RequestLogger, error) { + + hasInterceptorCaveat := func(caveats []string) bool { + for _, c := range caveats { + if strings.HasPrefix(c, macaroons.CondLndCustom) { + return true + } + } + + return false + } + + var shouldLogAction func(ri *RequestInfo) (bool, bool) + switch cfg.RequestLoggerLevel { + // Only log requests that have an interceptor caveat attached. + case RequestLoggerLevelInterceptor: + shouldLogAction = func(ri *RequestInfo) (bool, bool) { + if hasInterceptorCaveat(ri.Caveats) { + return true, true + } + + return false, false + } + + // Log all requests but only log request params if the request + // has an interceptor caveat. + case RequestLoggerLevelAll: + shouldLogAction = func(ri *RequestInfo) (bool, bool) { + return true, hasInterceptorCaveat(ri.Caveats) + } + + // Log all requests will all request parameters. + case RequestLoggerLevelFull: + shouldLogAction = func(ri *RequestInfo) (bool, bool) { + return true, true + } + + default: + return nil, fmt.Errorf("unknown request logger level: %s. "+ + "Expected either 'interceptor', 'all' or 'full'", + cfg.RequestLoggerLevel) + } + + return &RequestLogger{ + shouldLogAction: shouldLogAction, + actionsDB: actionsDB, + reqIDToAction: make(map[uint64]*firewalldb.ActionLocator), + }, nil +} + +// Name returns the name of the interceptor. +func (r *RequestLogger) Name() string { + return RequestLoggerName +} + +// ReadOnly returns true if this interceptor should be registered in read-only +// mode. In read-only mode no custom caveat name can be specified. +func (r *RequestLogger) ReadOnly() bool { + return true +} + +// CustomCaveatName returns the name of the custom caveat that is expected to be +// handled by this interceptor. Cannot be specified in read-only mode. +func (r *RequestLogger) CustomCaveatName() string { + return "" +} + +// Intercept processes an RPC middleware interception request and returns the +// interception result which either accepts or rejects the intercepted message. +func (r *RequestLogger) Intercept(_ context.Context, + req *lnrpc.RPCMiddlewareRequest) (*lnrpc.RPCMiddlewareResponse, error) { + + ri, err := NewInfoFromRequest(req) + if err != nil { + return nil, fmt.Errorf("error parsing incoming RPC middleware "+ + "interception request: %v", err) + } + + // If this request is for any URI in the uriSkipList map, then we do not + // log or persist it. + if uriSkipList[ri.URI] { + return mid.RPCOk(req) + } + + shouldLogAction, withPayloadData := r.shouldLogAction(ri) + if !shouldLogAction { + return mid.RPCOk(req) + } + + log.Tracef("RequestLogger: Intercepting %v", ri) + + switch ri.MWRequestType { + case MWRequestTypeStreamAuth: + return mid.RPCOk(req) + + // Parse incoming requests and act on them. + case MWRequestTypeRequest: + return mid.RPCErr(req, r.addNewAction(ri, withPayloadData)) + + // Parse and possibly manipulate outgoing responses. + case MWRequestTypeResponse: + var ( + state = firewalldb.ActionStateDone + errReason string + ) + if ri.IsError { + state = firewalldb.ActionStateError + errReason = mid.ParseResponseErr(ri.Serialized).Error() + } + + return mid.RPCErr( + req, r.MarkAction(ri.RequestID, state, errReason), + ) + + default: + return mid.RPCErrString(req, "invalid intercept type: %v", r) + } +} + +// addNewAction persists the new action to the db. +func (r *RequestLogger) addNewAction(ri *RequestInfo, + withPayloadData bool) error { + + // If no macaroon is provided, then an empty 4-byte array is used as the + // session ID. Otherwise, the macaroon is used to derive a session ID. + var sessionID [4]byte + if ri.Macaroon != nil { + var err error + sessionID, err = session.IDFromMacaroon(ri.Macaroon) + if err != nil { + return fmt.Errorf("could not extract ID from macaroon") + } + } + + action := &firewalldb.Action{ + RPCMethod: ri.URI, + AttemptedAt: time.Now(), + State: firewalldb.ActionStateInit, + } + + if withPayloadData { + msg, err := mid.ParseProtobuf(ri.GRPCMessageType, ri.Serialized) + if err != nil { + return err + } + + jsonMarshaler := &jsonpb.Marshaler{ + EmitDefaults: true, + OrigName: true, + } + + jsonStr, err := jsonMarshaler.MarshalToString( + proto.MessageV1(msg), + ) + if err != nil { + return fmt.Errorf("unable to decode response: %v", err) + } + + action.RPCParamsJson = []byte(jsonStr) + + meta := ri.MetaInfo + if meta != nil { + action.ActorName = meta.ActorName + action.FeatureName = meta.Feature + action.Trigger = meta.Trigger + action.Intent = meta.Intent + action.StructuredJsonData = meta.StructuredJsonData + } + } + + id, err := r.actionsDB.AddAction(sessionID, action) + if err != nil { + return err + } + + r.mu.Lock() + r.reqIDToAction[ri.RequestID] = &firewalldb.ActionLocator{ + SessionID: sessionID, + ActionID: id, + } + r.mu.Unlock() + + return nil +} + +// MarkAction can be used to set the state of an action identified by the given +// requestID. +func (r *RequestLogger) MarkAction(reqID uint64, + state firewalldb.ActionState, errReason string) error { + + r.mu.Lock() + defer r.mu.Unlock() + + actionLocator, ok := r.reqIDToAction[reqID] + if !ok { + return nil + } + delete(r.reqIDToAction, reqID) + + return r.actionsDB.SetActionState(actionLocator, state, errReason) +} diff --git a/firewall/rule_enforcer.go b/firewall/rule_enforcer.go new file mode 100644 index 000000000..8b2c03060 --- /dev/null +++ b/firewall/rule_enforcer.go @@ -0,0 +1,385 @@ +package firewall + +import ( + "context" + "fmt" + + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lightning-terminal/perms" + mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" + "github.com/lightninglabs/lightning-terminal/rules" + "github.com/lightninglabs/lightning-terminal/session" + "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +const ( + // RuleEnforcerName is the name of the RuleEnforcer interceptor. + RuleEnforcerName = "lit-macaroon-firewall" +) + +// A compile-time assertion that RuleEnforcer is a +// rpcmiddleware.RequestInterceptor. +var _ mid.RequestInterceptor = (*RuleEnforcer)(nil) + +// RuleEnforcer is a RequestInterceptor that makes sure all firewall related +// custom caveats in a macaroon are properly enforced. +type RuleEnforcer struct { + ruleDB firewalldb.RulesDB + actionsDB firewalldb.ActionReadDBGetter + markActionErrored func(reqID uint64, reason string) error + newPrivMap firewalldb.NewPrivacyMapDB + + permsMgr *perms.Manager + getFeaturePerms featurePerms + + nodeID [33]byte + + routerClient lndclient.RouterClient + lndClient lndclient.LightningClient + + ruleMgrs rules.ManagerSet +} + +// featurePerms defines the signature of a function that can be used to fetch +// feature permissions. +type featurePerms func(ctx context.Context) (map[string]map[string]bool, error) + +// NewRuleEnforcer constructs a new RuleEnforcer instance. +func NewRuleEnforcer(ruleDB firewalldb.RulesDB, + actionsDB firewalldb.ActionReadDBGetter, getFeaturePerms featurePerms, + permsMgr *perms.Manager, nodeID [33]byte, + routerClient lndclient.RouterClient, + lndClient lndclient.LightningClient, ruleMgrs rules.ManagerSet, + markActionErrored func(reqID uint64, reason string) error, + privMap firewalldb.NewPrivacyMapDB) *RuleEnforcer { + + return &RuleEnforcer{ + ruleDB: ruleDB, + actionsDB: actionsDB, + permsMgr: permsMgr, + getFeaturePerms: getFeaturePerms, + nodeID: nodeID, + routerClient: routerClient, + lndClient: lndClient, + ruleMgrs: ruleMgrs, + markActionErrored: markActionErrored, + newPrivMap: privMap, + } +} + +// Name returns the name of the interceptor. +func (r *RuleEnforcer) Name() string { + return RuleEnforcerName +} + +// ReadOnly returns true if this interceptor should be registered in read-only +// mode. In read-only mode no custom caveat name can be specified. +func (r *RuleEnforcer) ReadOnly() bool { + return false +} + +// CustomCaveatName returns the name of the custom caveat that is expected to be +// handled by this interceptor. Cannot be specified in read-only mode. +func (r *RuleEnforcer) CustomCaveatName() string { + return RuleEnforcerCaveat +} + +// Intercept processes an RPC middleware interception request and returns the +// interception result which either accepts or rejects the intercepted message. +func (r *RuleEnforcer) Intercept(ctx context.Context, + req *lnrpc.RPCMiddlewareRequest) (*lnrpc.RPCMiddlewareResponse, error) { + + ri, err := NewInfoFromRequest(req) + if err != nil { + return nil, fmt.Errorf("error parsing incoming RPC middleware "+ + "interception request: %v", err) + } + + if ri.Rules == nil { + return mid.RPCOk(req) + } + + log.Tracef("RuleEnforcer: Intercepting %v", ri) + + if ri.MetaInfo == nil { + return mid.RPCErrString(req, "missing MetaInfo") + } + + // Ensure that the specified feature name is one listed in the macaroon. + featureName := ri.MetaInfo.Feature + _, ok := ri.Rules.FeatureRules[featureName] + if len(ri.Rules.FeatureRules) != 0 && !ok { + return mid.RPCErrString(req, "feature %s does not correspond "+ + "to a feature specified in the macaroon caveat", + featureName) + } + + // Ensure that the feature specified in the MetaInfo is one that we + // know about from our last interaction with the Autopilot server. + featurePerms, err := r.getFeaturePerms(ctx) + if err != nil { + return mid.RPCErrString(req, "unable to get feature "+ + "permissions") + } + + perms, ok := featurePerms[featureName] + if !ok { + return mid.RPCErrString(req, "feature %s is not a known "+ + "feature", featureName) + } + + // Then check that this URI is allowed given the list of perms the + // Autopilot told us this feature could use. + if !perms[ri.URI] { + return mid.RPCErrString(req, "Method %s is not allowed for "+ + "feature %s", ri.URI, featureName) + } + + switch ri.MWRequestType { + case MWRequestTypeStreamAuth: + return mid.RPCOk(req) + + // Parse incoming requests and act on them. + case MWRequestTypeRequest: + replacement, err := r.handleRequest(ctx, ri) + if err != nil { + dbErr := r.markActionErrored(ri.RequestID, err.Error()) + if dbErr != nil { + log.Error("could not mark action for "+ + "request ID %d as Errored: %v", + ri.RequestID, dbErr) + } + + return mid.RPCErr(req, err) + } + + // No error occurred but the request should be replaced with + // the given custom request. Wrap it in the correct RPC + // request of the interceptor now. + if replacement != nil { + return mid.RPCReplacement(req, replacement) + } + + // No error and no replacement, just return an empty request of + // the correct type. + return mid.RPCOk(req) + + // Parse and possibly manipulate outgoing responses. + case MWRequestTypeResponse: + if ri.IsError { + replacementErr, err := r.handleErrorResponse(ctx, ri) + if err != nil { + return mid.RPCErr(req, err) + } + + // No error occurred but the response error should be + // replaced with the given custom error. Wrap it in the + // correct RPC response of the interceptor now. + if replacementErr != nil { + return mid.RPCErrReplacement( + req, replacementErr, + ) + } + + // No error and no replacement, just return an empty + // response of the correct type. + return mid.RPCOk(req) + } + + replacement, err := r.handleResponse(ctx, ri) + if err != nil { + return mid.RPCErr(req, err) + } + + // No error occurred but the response should be replaced with + // the given custom response. Wrap it in the correct RPC + // response of the interceptor now. + if replacement != nil { + return mid.RPCReplacement(req, replacement) + } + + // No error and no replacement, just return an empty response of + // the correct type. + return mid.RPCOk(req) + + default: + return mid.RPCErrString(req, "invalid intercept type: %v", r) + } +} + +// handleRequest gathers the rules that will need to enforced for the given +// feature and runs the request against each of those. +func (r *RuleEnforcer) handleRequest(ctx context.Context, + ri *RequestInfo) (proto.Message, error) { + + sessionID, err := session.IDFromMacaroon(ri.Macaroon) + if err != nil { + return nil, fmt.Errorf("could not extract ID from macaroon") + } + + rules, err := r.collectEnforcers(ri, sessionID) + if err != nil { + return nil, fmt.Errorf("error parsing rules: %v", err) + } + + msg, err := mid.ParseProtobuf( + ri.GRPCMessageType, ri.Serialized, + ) + if err != nil { + return nil, fmt.Errorf("error parsing proto: %v", err) + } + + for _, rule := range rules { + newRequest, err := rule.HandleRequest(ctx, ri.URI, msg) + if err != nil { + st := status.Errorf( + codes.ResourceExhausted, "rule violation: %v", + err, + ) + return nil, st + } + + if newRequest != nil { + msg = newRequest + } + } + + return nil, nil +} + +// handleResponse gathers the rules that will need to be enforced for the given +// feature and runs the response against each of those. +func (r *RuleEnforcer) handleResponse(ctx context.Context, + ri *RequestInfo) (proto.Message, error) { + + sessionID, err := session.IDFromMacaroon(ri.Macaroon) + if err != nil { + return nil, fmt.Errorf("could not extract ID from macaroon") + } + + enforcers, err := r.collectEnforcers(ri, sessionID) + if err != nil { + return nil, fmt.Errorf("error parsing rules: %v", err) + } + + msg, err := mid.ParseProtobuf(ri.GRPCMessageType, ri.Serialized) + if err != nil { + return nil, fmt.Errorf("error parsing proto: %v", err) + } + + for _, enforcer := range enforcers { + newResponse, err := enforcer.HandleResponse(ctx, ri.URI, msg) + if err != nil { + return nil, err + } + + if newResponse != nil { + msg = newResponse + } + } + + return msg, nil +} + +// handleErrorResponse gathers the rules that will need to be enforced for the +// given feature and runs the response error against each of those. +func (r *RuleEnforcer) handleErrorResponse(ctx context.Context, + ri *RequestInfo) (error, error) { + + sessionID, err := session.IDFromMacaroon(ri.Macaroon) + if err != nil { + return nil, fmt.Errorf("could not extract ID from macaroon") + } + + enforcers, err := r.collectEnforcers(ri, sessionID) + if err != nil { + return nil, fmt.Errorf("error parsing rules: %v", err) + } + + parsedErr := mid.ParseResponseErr(ri.Serialized) + + for _, enforcer := range enforcers { + newErr, err := enforcer.HandleErrorResponse( + ctx, ri.URI, parsedErr, + ) + if err != nil { + return nil, err + } + + if newErr != nil { + parsedErr = newErr + } + } + + return parsedErr, nil +} + +// collectRule initialises and returns all the Rules that need to be enforced +// for the given request. +func (r *RuleEnforcer) collectEnforcers(ri *RequestInfo, sessionID session.ID) ( + []rules.Enforcer, error) { + + ruleEnforcers := make( + []rules.Enforcer, 0, + len(ri.Rules.FeatureRules)+len(ri.Rules.SessionRules), + ) + + for rule, value := range ri.Rules.FeatureRules[ri.MetaInfo.Feature] { + r, err := r.initRule( + ri.RequestID, rule, []byte(value), ri.MetaInfo.Feature, + sessionID, false, ri.WithPrivacy, + ) + if err != nil { + return nil, err + } + + ruleEnforcers = append(ruleEnforcers, r) + } + + return ruleEnforcers, nil +} + +// initRule initialises a rule.Rule with any required config values. +func (r *RuleEnforcer) initRule(reqID uint64, name string, value []byte, + featureName string, sessionID session.ID, sessionRule, + privacy bool) (rules.Enforcer, error) { + + ruleValues, err := r.ruleMgrs.InitRuleValues(name, value) + if err != nil { + return nil, err + } + + if privacy { + privMap := r.newPrivMap(sessionID) + ruleValues, err = ruleValues.PseudoToReal(privMap) + if err != nil { + return nil, fmt.Errorf("could not prepare rule "+ + "value: %v", err) + } + } + + allActionsDB := r.actionsDB.GetActionsReadDB(sessionID, featureName) + actionsDB := allActionsDB.FeatureActionsDB() + rulesDB := r.ruleDB.GetKVStores(name, sessionID, featureName) + + if sessionRule { + actionsDB = allActionsDB.SessionActionsDB() + rulesDB = r.ruleDB.GetKVStores(name, sessionID, "") + } + + cfg := &rules.ConfigImpl{ + Stores: rulesDB, + ActionsDB: actionsDB, + MethodPerms: r.permsMgr.URIPermissions, + NodeID: r.nodeID, + RouterClient: r.routerClient, + LndClient: r.lndClient, + ReqID: int64(reqID), + } + + return r.ruleMgrs.InitEnforcer(cfg, name, ruleValues) +} diff --git a/firewalldb/action_paginator.go b/firewalldb/action_paginator.go new file mode 100644 index 000000000..5e492f55e --- /dev/null +++ b/firewalldb/action_paginator.go @@ -0,0 +1,250 @@ +package firewalldb + +import ( + "encoding/binary" + + "github.com/lightningnetwork/lnd/kvdb" +) + +type actionPaginator struct { + // cursor is the cursor which we are using to iterate through a bucket. + cursor kvdb.RCursor + + // cfg is the query config which we are using to determine how to + // iterate over the data. + cfg *ListActionsQuery + + // filterFn is the filter function which we are using to determine which + // actions should be included in the return list. + filterFn ListActionsFilterFn + + // readAction is a closure which we use to read an action from the db + // given a key value pair. + readAction func(k, v []byte) (*Action, error) +} + +// paginateActions paginates through the set of actions in the database. It +// uses the provided cursor to determine which keys to iterate over, it uses the +// provided query options to modify how the iteration is done, and it uses the +// filter function to determine which actions to include in the result. +// It returns the list of selected actions, the last index that was read from, +// and the total number of actions that matched the filter function (iff +// cfg.CountAll is set). +func paginateActions(cfg *ListActionsQuery, c kvdb.RCursor, + readAction func(k, v []byte) (*Action, error), + filterFn ListActionsFilterFn) ([]*Action, uint64, uint64, error) { + + if cfg == nil { + cfg = &ListActionsQuery{} + } + + if filterFn == nil { + filterFn = func(a *Action, reversed bool) (bool, bool) { + return true, true + } + } + + p := actionPaginator{ + cfg: cfg, + cursor: c, + readAction: readAction, + filterFn: filterFn, + } + + if cfg.CountAll { + return p.queryCountAll() + } + + actions, lastIndex, err := p.query() + + return actions, lastIndex, 0, err +} + +// keyValueForIndex seeks our cursor to a given index and returns the key and +// value at that position. +func (p *actionPaginator) keyValueForIndex(index uint64) ([]byte, []byte) { + var keyIndex [8]byte + byteOrder.PutUint64(keyIndex[:], index) + return p.cursor.Seek(keyIndex[:]) +} + +// lastIndex returns the last value in our index, if our index is empty it +// returns 0. +func (p *actionPaginator) lastIndex() uint64 { + keyIndex, _ := p.cursor.Last() + if keyIndex == nil { + return 0 + } + + return byteOrder.Uint64(keyIndex) +} + +// nextKey is a helper closure to determine what key we should use next when +// we are iterating, depending on whether we are iterating forwards or in +// reverse. +func (p *actionPaginator) nextKey() ([]byte, []byte) { + if p.cfg.Reversed { + return p.cursor.Prev() + } + return p.cursor.Next() +} + +// cursorStart gets the index key and value for the first item we are looking +// up, taking into account that we may be paginating in reverse. The index +// offset provided is *excusive* so we will start with the item after the offset +// for forwards queries, and the item before the index for backwards queries. +func (p *actionPaginator) cursorStart() ([]byte, []byte) { + indexKey, indexValue := p.keyValueForIndex(p.cfg.IndexOffset + 1) + + // If the query is specifying reverse iteration, then we must + // handle a few offset cases. + if p.cfg.Reversed { + switch { + // This indicates the default case, where no offset was + // specified. In that case we just start from the last + // entry. + case p.cfg.IndexOffset == 0: + indexKey, indexValue = p.cursor.Last() + + // This indicates the offset being set to the very + // first entry. Since there are no entries before + // this offset, and the direction is reversed, we can + // return without adding any invoices to the response. + case p.cfg.IndexOffset == 1: + return nil, nil + + // If we have been given an index offset that is beyond our last + // index value, we just return the last indexed value in our set + // since we are querying in reverse. We do not cover the case + // where our index offset equals our last index value, because + // index offset is exclusive, so we would want to start at the + // value before our last index. + case p.cfg.IndexOffset > p.lastIndex(): + return p.cursor.Last() + + // Otherwise we have an index offset which is within our set of + // indexed keys, and we want to start at the item before our + // offset. We seek to our index offset, then return the element + // before it. We do this rather than p.indexOffset-1 to account + // for indexes that have gaps. + default: + p.keyValueForIndex(p.cfg.IndexOffset) + indexKey, indexValue = p.cursor.Prev() + } + } + + return indexKey, indexValue +} + +// query gets the start point for our index offset and iterates through keys +// in our index until we reach the total number of items required for the query +// or we run out of cursor values. This function takes a fetchAndAppend function +// which is responsible for looking up the entry at that index, adding the entry +// to its set of return items (if desired) and return a boolean which indicates +// whether the item was added. This is required to allow the actionPaginator to +// determine when the response has the maximum number of required items. +func (p *actionPaginator) query() ([]*Action, uint64, error) { + indexKey, indexValue := p.cursorStart() + + var ( + actions []*Action + lastIndex = uint64(1) + ) + for ; indexKey != nil; indexKey, indexValue = p.nextKey() { + // If our current return payload exceeds the max number + // of invoices, then we'll exit now. + if p.cfg.MaxNum != 0 && + uint64(len(actions)) >= p.cfg.MaxNum { + + break + } + + lastIndex = binary.BigEndian.Uint64(indexKey) + + action, err := p.readAction(indexKey, indexValue) + if err != nil { + return nil, 0, err + } + + add, cont := p.filterFn(action, p.cfg.Reversed) + if !cont { + break + } + + if !add { + continue + } + + actions = append(actions, action) + } + + return actions, lastIndex, nil +} + +// queryCountAll is similar to query except that instead of only iterating over +// a limited set of actions (as defined by the cfg.IndexOffset and cfg.MaxNum), +// it will instead iterate through all actions so that it can count the total +// number of actions that match the filter function. It will however only +// return actions in the range specified by the cfg.IndexOffset and cfg.MaxNum. +// Callers should be aware that this is a much slower function than query if +// there are a large number of actions in the database. +func (p *actionPaginator) queryCountAll() ([]*Action, uint64, uint64, error) { + // Start at the very first, or very last item. + indexKey, indexValue := p.cursor.First() + if p.cfg.Reversed { + indexKey, indexValue = p.cursor.Last() + } + // Then iterate from first to last and check each action. If passes + // filter, increment total count. Only if the current index is after + // (or before (in reverse mode)) the offset do we add the action & that + // is only if the num we have collected is below MaxNum. + + var ( + actions []*Action + lastIndex = uint64(1) + beforeIndexOffset = p.cfg.IndexOffset != 0 + totalCount uint64 + ) + for ; indexKey != nil; indexKey, indexValue = p.nextKey() { + action, err := p.readAction(indexKey, indexValue) + if err != nil { + return nil, 0, 0, err + } + + add, cont := p.filterFn(action, p.cfg.Reversed) + if !cont { + break + } + + if !add { + continue + } + + totalCount++ + + if p.cfg.IndexOffset != 0 && + binary.BigEndian.Uint64(indexKey) == p.cfg.IndexOffset+1 { + + beforeIndexOffset = false + } + + // Don't add the action if we are still before the offset. + if beforeIndexOffset { + continue + } + + // If our current return payload exceeds the max number + // of invoices, then we continue without adding the action to + // our return list. + if p.cfg.MaxNum != 0 && + uint64(len(actions)) >= p.cfg.MaxNum { + + continue + } + + lastIndex = binary.BigEndian.Uint64(indexKey) + actions = append(actions, action) + } + + return actions, lastIndex, totalCount, nil +} diff --git a/firewalldb/actions.go b/firewalldb/actions.go new file mode 100644 index 000000000..0e491af86 --- /dev/null +++ b/firewalldb/actions.go @@ -0,0 +1,677 @@ +package firewalldb + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "io" + "time" + + "github.com/lightninglabs/lightning-terminal/session" + "github.com/lightningnetwork/lnd/tlv" + "go.etcd.io/bbolt" +) + +const ( + typeActorName tlv.Type = 1 + typeFeature tlv.Type = 2 + typeTrigger tlv.Type = 3 + typeIntent tlv.Type = 4 + typeStructuredJsonData tlv.Type = 5 + typeRPCMethod tlv.Type = 6 + typeRPCParamsJson tlv.Type = 7 + typeAttemptedAt tlv.Type = 8 + typeState tlv.Type = 9 + typeErrorReason tlv.Type = 10 + + typeLocatorSessionID tlv.Type = 1 + typeLocatorActionID tlv.Type = 2 +) + +/* + The Actions are stored in the following structure in the KV db: + + actions-bucket -> actions -> -> -> serialised action + + -> actions-index -> -> {sessionID:action-index} +*/ + +var ( + // actionsBucketKey is the key that will be used for the main Actions + // bucket. + actionsBucketKey = []byte("actions-bucket") + + // actionsKey is the key used for the sub-bucket containing the + // session actions. + actionsKey = []byte("actions") + + // actionsIndex is the key used for the sub-bucket containing a map + // from monotonically increasing IDs to action locators. + actionsIndex = []byte("actions-index") +) + +// ActionState represents the state of an action. +type ActionState uint8 + +const ( + // ActionStateUnknown means that the action's state was never + // initialised. This should never be the case. + ActionStateUnknown ActionState = 0 + + // ActionStateInit represents that an Action has been created but that + // is still in the pending state. + ActionStateInit ActionState = 1 + + // ActionStateDone represents that an Action has been executed + // successfully. + ActionStateDone ActionState = 2 + + // ActionStateError represents that an Action did not complete + // successfully. + ActionStateError ActionState = 3 +) + +// Action represents an RPC call made through the firewall. +type Action struct { + // SessionID is the ID of the session that this action belongs to. + // Note that this is not serialized on persistence since the action is + // already stored under a bucket identified by the session ID. + SessionID session.ID + + // ActorName is the name of the entity who performed the Action. + ActorName string + + // FeatureName is the name of the feature that the Action is being + // performed by. + FeatureName string + + // Trigger is the meta info detailing what caused this action to be + // executed. + Trigger string + + // Intent is the meta info detailing what the intended outcome of this + // action will be. + Intent string + + // StructuredJsonData is extra, structured, info that the Autopilot can + // send to Litd serialised as a json string. + StructuredJsonData string + + // RPCMethod is the URI that was called. + RPCMethod string + + // RPCParams is the method parameters of the request in JSON form. + RPCParamsJson []byte + + // AttemptedAt is the time at which this action was created. + AttemptedAt time.Time + + // State represents the state of the Action. + State ActionState + + // ErrorReason is the human-readable reason for why the action failed. + // It will only be set if State is ActionStateError. + ErrorReason string +} + +// AddAction serialises and adds an Action to the DB under the given sessionID. +func (db *DB) AddAction(sessionID session.ID, action *Action) (uint64, error) { + var buf bytes.Buffer + if err := SerializeAction(&buf, action); err != nil { + return 0, err + } + + var id uint64 + err := db.DB.Update(func(tx *bbolt.Tx) error { + mainActionsBucket, err := getBucket(tx, actionsBucketKey) + if err != nil { + return err + } + + actionsBucket := mainActionsBucket.Bucket(actionsKey) + if actionsBucket == nil { + return ErrNoSuchKeyFound + } + + sessBucket, err := actionsBucket.CreateBucketIfNotExists( + sessionID[:], + ) + if err != nil { + return err + } + + nextActionIndex, err := sessBucket.NextSequence() + if err != nil { + return err + } + id = nextActionIndex + + var actionIndex [8]byte + byteOrder.PutUint64(actionIndex[:], nextActionIndex) + err = sessBucket.Put(actionIndex[:], buf.Bytes()) + if err != nil { + return err + } + + actionsIndexBucket := mainActionsBucket.Bucket(actionsIndex) + if actionsIndexBucket == nil { + return ErrNoSuchKeyFound + } + + nextSeq, err := actionsIndexBucket.NextSequence() + if err != nil { + return err + } + + locator := ActionLocator{ + SessionID: sessionID, + ActionID: nextActionIndex, + } + + var buf bytes.Buffer + err = serializeActionLocator(&buf, &locator) + if err != nil { + return err + } + + var seqNoBytes [8]byte + byteOrder.PutUint64(seqNoBytes[:], nextSeq) + return actionsIndexBucket.Put(seqNoBytes[:], buf.Bytes()) + }) + if err != nil { + return 0, err + } + + return id, nil +} + +func putAction(tx *bbolt.Tx, al *ActionLocator, a *Action) error { + var buf bytes.Buffer + if err := SerializeAction(&buf, a); err != nil { + return err + } + + mainActionsBucket, err := getBucket(tx, actionsBucketKey) + if err != nil { + return err + } + + actionsBucket := mainActionsBucket.Bucket(actionsKey) + if actionsBucket == nil { + return ErrNoSuchKeyFound + } + + sessBucket := actionsBucket.Bucket(al.SessionID[:]) + if sessBucket == nil { + return fmt.Errorf("session bucket for session ID %x does not "+ + "exist", al.SessionID) + } + + var id [8]byte + binary.BigEndian.PutUint64(id[:], al.ActionID) + + return sessBucket.Put(id[:], buf.Bytes()) +} + +func getAction(actionsBkt *bbolt.Bucket, al *ActionLocator) (*Action, error) { + sessBucket := actionsBkt.Bucket(al.SessionID[:]) + if sessBucket == nil { + return nil, fmt.Errorf("session bucket for session ID "+ + "%x does not exist", al.SessionID) + } + + var id [8]byte + binary.BigEndian.PutUint64(id[:], al.ActionID) + + actionBytes := sessBucket.Get(id[:]) + return DeserializeAction(bytes.NewReader(actionBytes), al.SessionID) +} + +// SetActionState finds the action specified by the ActionLocator and sets its +// state to the given state. +func (db *DB) SetActionState(al *ActionLocator, state ActionState, + errorReason string) error { + + if errorReason != "" && state != ActionStateError { + return fmt.Errorf("error reason should only be set for " + + "ActionStateError") + } + + return db.DB.Update(func(tx *bbolt.Tx) error { + mainActionsBucket, err := getBucket(tx, actionsBucketKey) + if err != nil { + return err + } + + actionsBucket := mainActionsBucket.Bucket(actionsKey) + if actionsBucket == nil { + return ErrNoSuchKeyFound + } + + action, err := getAction(actionsBucket, al) + if err != nil { + return err + } + + action.State = state + action.ErrorReason = errorReason + + return putAction(tx, al, action) + }) +} + +// ListActionsQuery can be used to tweak the query to ListActions and +// ListSessionActions. +type ListActionsQuery struct { + // IndexOffset is index of the action to inspect. + IndexOffset uint64 + + // MaxNum is the maximum number of actions to return. If it is set to 0, + // then no maximum is enforced. + MaxNum uint64 + + // Reversed indicates whether the actions should be returned in reverse + // order. + Reversed bool + + // CountAll should be set to true if the total number of actions that + // satisfy the query should be counted and returned. Note that this will + // make the query slower. + CountAll bool +} + +// ListActionsFilterFn defines a function that can be used to determine if an +// action should be included in a set of results or not. The reversed parameter +// indicates if the actions are being traversed in reverse order or not. +// The first return boolean indicates if the action should be included or not +// and the second one indicates if the iteration should be stopped or not. +type ListActionsFilterFn func(a *Action, reversed bool) (bool, bool) + +// ListActions returns a list of Actions that pass the filterFn requirements. +// The indexOffset and maxNum params can be used to control the number of +// actions returned. The return values are the list of actions, the last index +// and the total count (iff query.CountTotal is set). +func (db *DB) ListActions(filterFn ListActionsFilterFn, + query *ListActionsQuery) ([]*Action, uint64, uint64, error) { + + var ( + actions []*Action + totalCount uint64 + lastIndex uint64 + ) + err := db.View(func(tx *bbolt.Tx) error { + mainActionsBucket, err := getBucket(tx, actionsBucketKey) + if err != nil { + return err + } + + actionsBucket := mainActionsBucket.Bucket(actionsKey) + if actionsBucket == nil { + return ErrNoSuchKeyFound + } + + actionsIndexBucket := mainActionsBucket.Bucket(actionsIndex) + if actionsIndexBucket == nil { + return ErrNoSuchKeyFound + } + + readAction := func(index, locatorBytes []byte) (*Action, + error) { + + locator, err := deserializeActionLocator( + bytes.NewReader(locatorBytes), + ) + if err != nil { + return nil, err + } + + return getAction(actionsBucket, locator) + } + + actions, lastIndex, totalCount, err = paginateActions( + query, actionsIndexBucket.Cursor(), readAction, + filterFn, + ) + return err + }) + if err != nil { + return nil, 0, 0, err + } + + return actions, lastIndex, totalCount, nil +} + +// ListSessionActions returns a list of the given session's Actions that pass +// the filterFn requirements. +func (db *DB) ListSessionActions(sessionID session.ID, + filterFn ListActionsFilterFn, query *ListActionsQuery) ([]*Action, + uint64, uint64, error) { + + var ( + actions []*Action + totalCount uint64 + lastIndex uint64 + ) + err := db.View(func(tx *bbolt.Tx) error { + mainActionsBucket, err := getBucket(tx, actionsBucketKey) + if err != nil { + return err + } + + actionsBucket := mainActionsBucket.Bucket(actionsKey) + if actionsBucket == nil { + return ErrNoSuchKeyFound + } + + sessionsBucket := actionsBucket.Bucket(sessionID[:]) + if sessionsBucket == nil { + return nil + } + + readAction := func(_, v []byte) (*Action, error) { + return DeserializeAction(bytes.NewReader(v), sessionID) + } + + actions, lastIndex, totalCount, err = paginateActions( + query, sessionsBucket.Cursor(), readAction, filterFn, + ) + + return err + }) + if err != nil { + return nil, 0, 0, err + } + + return actions, lastIndex, totalCount, nil +} + +// SerializeAction binary serializes the given action to the writer using the +// tlv format. +func SerializeAction(w io.Writer, action *Action) error { + if action == nil { + return fmt.Errorf("action cannot be nil") + } + + var ( + actor = []byte(action.ActorName) + feature = []byte(action.FeatureName) + trigger = []byte(action.Trigger) + intent = []byte(action.Intent) + data = []byte(action.StructuredJsonData) + rpcMethod = []byte(action.RPCMethod) + params = action.RPCParamsJson + attemptedAt = uint64(action.AttemptedAt.Unix()) + state = uint8(action.State) + errorReason = []byte(action.ErrorReason) + ) + + tlvRecords := []tlv.Record{ + tlv.MakePrimitiveRecord(typeActorName, &actor), + tlv.MakePrimitiveRecord(typeFeature, &feature), + tlv.MakePrimitiveRecord(typeTrigger, &trigger), + tlv.MakePrimitiveRecord(typeIntent, &intent), + tlv.MakePrimitiveRecord(typeStructuredJsonData, &data), + tlv.MakePrimitiveRecord(typeRPCMethod, &rpcMethod), + tlv.MakePrimitiveRecord(typeRPCParamsJson, ¶ms), + tlv.MakePrimitiveRecord(typeAttemptedAt, &attemptedAt), + tlv.MakePrimitiveRecord(typeState, &state), + tlv.MakePrimitiveRecord(typeErrorReason, &errorReason), + } + + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// DeserializeAction deserializes an action from the given reader, expecting +// the data to be encoded in the tlv format. +func DeserializeAction(r io.Reader, sessionID session.ID) (*Action, error) { + var ( + action = Action{} + actor, featureName []byte + trigger, intent, data []byte + rpcMethod, params []byte + attemptedAt uint64 + state uint8 + errorReason []byte + ) + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord(typeActorName, &actor), + tlv.MakePrimitiveRecord(typeFeature, &featureName), + tlv.MakePrimitiveRecord(typeTrigger, &trigger), + tlv.MakePrimitiveRecord(typeIntent, &intent), + tlv.MakePrimitiveRecord(typeStructuredJsonData, &data), + tlv.MakePrimitiveRecord(typeRPCMethod, &rpcMethod), + tlv.MakePrimitiveRecord(typeRPCParamsJson, ¶ms), + tlv.MakePrimitiveRecord(typeAttemptedAt, &attemptedAt), + tlv.MakePrimitiveRecord(typeState, &state), + tlv.MakePrimitiveRecord(typeErrorReason, &errorReason), + ) + if err != nil { + return nil, err + } + + _, err = tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return nil, err + } + + action.SessionID = sessionID + action.ActorName = string(actor) + action.FeatureName = string(featureName) + action.Trigger = string(trigger) + action.Intent = string(intent) + action.StructuredJsonData = string(data) + action.RPCMethod = string(rpcMethod) + action.RPCParamsJson = params + action.AttemptedAt = time.Unix(int64(attemptedAt), 0) + action.State = ActionState(state) + action.ErrorReason = string(errorReason) + + return &action, nil +} + +// ActionsWriteDB is an abstraction over the Actions DB that will allow a +// caller to add new actions as well as change the values of an existing action. +type ActionsWriteDB interface { + AddAction(sessionID session.ID, action *Action) (uint64, error) + SetActionState(al *ActionLocator, state ActionState, + errReason string) error +} + +// RuleAction represents a method call that was performed at a certain time at +// a certain time. +type RuleAction struct { + // Method is the URI of the rpc method that was called. + Method string + + // PerformedAt is the time at which the action was attempted. + PerformedAt time.Time +} + +// ActionsDB represents a DB backend that contains Action entries that can +// be queried. It allows us to abstract away the details of the data storage +// method. +type ActionsDB interface { + // ListActions returns a list of past Action items. + ListActions(ctx context.Context) ([]*RuleAction, error) +} + +// ActionsReadDB is an abstraction gives a caller access to either a session +// specific or feature specific rules.ActionDB +type ActionsReadDB interface { + SessionActionsDB() ActionsDB + FeatureActionsDB() ActionsDB +} + +// ActionReadDBGetter represents a function that can be used to construct +// an ActionsReadDB. +type ActionReadDBGetter interface { + GetActionsReadDB(sessionID session.ID, featureName string) ActionsReadDB +} + +// GetActionsReadDB is a method on DB that constructs an ActionsReadDB. +func (db *DB) GetActionsReadDB(sessionID session.ID, + featureName string) ActionsReadDB { + + return &allActionsReadDB{ + db: db, + sessionID: sessionID, + featureName: featureName, + } +} + +// allActionsReadDb is an implementation of the ActionsReadDB. +type allActionsReadDB struct { + db *DB + sessionID session.ID + featureName string +} + +var _ ActionsReadDB = (*allActionsReadDB)(nil) + +// SessionActionsDB returns a rules.ActionsDB that will give the caller access +// to all of a sessions Actions. +func (a *allActionsReadDB) SessionActionsDB() ActionsDB { + return &sessionActionsReadDB{a} +} + +// FeatureActionsDB returns an rules.ActionsDB that will give the caller access +// to only a specific features Actions in a specific session. +func (a *allActionsReadDB) FeatureActionsDB() ActionsDB { + return &featureActionsReadDB{a} +} + +// sessionActionReadDB is an implementation of the rules.ActionsDB that will +// provide read access to all the Actions of a particular session. +type sessionActionsReadDB struct { + *allActionsReadDB +} + +var _ ActionsDB = (*sessionActionsReadDB)(nil) + +// ListActions will return all the Actions for a particular session. +func (s *sessionActionsReadDB) ListActions(_ context.Context) ([]*RuleAction, + error) { + + sessionActions, _, _, err := s.db.ListSessionActions( + s.sessionID, func(a *Action, _ bool) (bool, bool) { + return a.State == ActionStateDone, true + }, nil, + ) + if err != nil { + return nil, err + } + + actions := make([]*RuleAction, len(sessionActions)) + for i, action := range sessionActions { + actions[i] = actionToRulesAction(action) + } + + return actions, nil +} + +// featureActionReadDB is an implementation of the rules.ActionsDB that will +// provide read access to all the Actions of a feature within a particular +// session. +type featureActionsReadDB struct { + *allActionsReadDB +} + +var _ ActionsDB = (*featureActionsReadDB)(nil) + +// ListActions will return all the Actions for a particular session that were +// executed by a particular feature. +func (a *featureActionsReadDB) ListActions(_ context.Context) ([]*RuleAction, + error) { + + featureActions, _, _, err := a.db.ListSessionActions( + a.sessionID, func(action *Action, _ bool) (bool, bool) { + return action.State == ActionStateDone && + action.FeatureName == a.featureName, true + }, nil, + ) + if err != nil { + return nil, err + } + + actions := make([]*RuleAction, len(featureActions)) + for i, action := range featureActions { + actions[i] = actionToRulesAction(action) + } + + return actions, nil +} + +func actionToRulesAction(a *Action) *RuleAction { + return &RuleAction{ + Method: a.RPCMethod, + PerformedAt: a.AttemptedAt, + } +} + +// ActionLocator helps us find an action in the database. +type ActionLocator struct { + SessionID session.ID + ActionID uint64 +} + +// serializeActionLocator binary serializes the given ActionLocator to the +// writer using the tlv format. +func serializeActionLocator(w io.Writer, al *ActionLocator) error { + if al == nil { + return fmt.Errorf("action locator cannot be nil") + } + + var ( + sessionID = al.SessionID[:] + actionID = al.ActionID + ) + + tlvRecords := []tlv.Record{ + tlv.MakePrimitiveRecord(typeLocatorSessionID, &sessionID), + tlv.MakePrimitiveRecord(typeLocatorActionID, &actionID), + } + + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// deserializeActionLocator deserializes an ActionLocator from the given reader, +// expecting the data to be encoded in the tlv format. +func deserializeActionLocator(r io.Reader) (*ActionLocator, error) { + var ( + sessionID []byte + actionID uint64 + ) + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord(typeLocatorSessionID, &sessionID), + tlv.MakePrimitiveRecord(typeLocatorActionID, &actionID), + ) + if err != nil { + return nil, err + } + + _, err = tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return nil, err + } + + id, err := session.IDFromBytes(sessionID) + if err != nil { + return nil, err + } + + return &ActionLocator{ + SessionID: id, + ActionID: actionID, + }, nil +} diff --git a/firewalldb/actions_test.go b/firewalldb/actions_test.go new file mode 100644 index 000000000..e0502903d --- /dev/null +++ b/firewalldb/actions_test.go @@ -0,0 +1,337 @@ +package firewalldb + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// TestActionStorage tests that the ActionsDB CRUD logic. +func TestActionStorage(t *testing.T) { + tmpDir := t.TempDir() + + db, err := NewDB(tmpDir, "test.db") + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) + + sessionID1 := [4]byte{1, 1, 1, 1} + action1 := &Action{ + SessionID: sessionID1, + ActorName: "Autopilot", + FeatureName: "auto-fees", + Trigger: "fee too low", + Intent: "increase fee", + StructuredJsonData: "{\"something\":\"nothing\"}", + RPCMethod: "UpdateChanPolicy", + RPCParamsJson: []byte("new fee"), + AttemptedAt: time.Unix(32100, 0), + State: ActionStateDone, + } + + sessionID2 := [4]byte{2, 2, 2, 2} + action2 := &Action{ + SessionID: sessionID2, + ActorName: "Autopilot", + FeatureName: "rebalancer", + Trigger: "channels not balanced", + Intent: "balance", + RPCMethod: "SendToRoute", + RPCParamsJson: []byte("hops, amount"), + AttemptedAt: time.Unix(12300, 0), + State: ActionStateInit, + } + + actionsStateFilterFn := func(state ActionState) ListActionsFilterFn { + return func(a *Action, _ bool) (bool, bool) { + return a.State == state, true + } + } + + actions, _, _, err := db.ListSessionActions( + sessionID1, actionsStateFilterFn(ActionStateDone), nil, + ) + require.NoError(t, err) + require.Len(t, actions, 0) + + actions, _, _, err = db.ListSessionActions( + sessionID2, actionsStateFilterFn(ActionStateDone), nil, + ) + require.NoError(t, err) + require.Len(t, actions, 0) + + id, err := db.AddAction(sessionID1, action1) + require.NoError(t, err) + require.Equal(t, uint64(1), id) + + id, err = db.AddAction(sessionID2, action2) + require.NoError(t, err) + require.Equal(t, uint64(1), id) + + actions, _, _, err = db.ListSessionActions( + sessionID1, actionsStateFilterFn(ActionStateDone), nil, + ) + require.NoError(t, err) + require.Len(t, actions, 1) + require.Equal(t, action1, actions[0]) + + actions, _, _, err = db.ListSessionActions( + sessionID2, actionsStateFilterFn(ActionStateDone), nil, + ) + require.NoError(t, err) + require.Len(t, actions, 0) + + err = db.SetActionState( + &ActionLocator{ + SessionID: sessionID2, + ActionID: uint64(1), + }, ActionStateDone, "", + ) + require.NoError(t, err) + + actions, _, _, err = db.ListSessionActions( + sessionID2, actionsStateFilterFn(ActionStateDone), nil, + ) + require.NoError(t, err) + require.Len(t, actions, 1) + action2.State = ActionStateDone + require.Equal(t, action2, actions[0]) + + id, err = db.AddAction(sessionID1, action1) + require.NoError(t, err) + require.Equal(t, uint64(2), id) + + // Check that providing no session id and no filter function returns + // all the actions. + actions, _, _, err = db.ListActions(nil, &ListActionsQuery{ + IndexOffset: 0, + MaxNum: 100, + Reversed: false, + }) + require.NoError(t, err) + require.Len(t, actions, 3) + + // Try set an error reason for a non Errored state. + err = db.SetActionState( + &ActionLocator{ + SessionID: sessionID2, + ActionID: uint64(1), + }, ActionStateDone, "hello", + ) + require.Error(t, err) + + // Now try move the action to errored with a reason. + err = db.SetActionState( + &ActionLocator{ + SessionID: sessionID2, + ActionID: uint64(1), + }, ActionStateError, "fail whale", + ) + require.NoError(t, err) + + actions, _, _, err = db.ListSessionActions( + sessionID2, actionsStateFilterFn(ActionStateError), nil, + ) + require.NoError(t, err) + require.Len(t, actions, 1) + action2.State = ActionStateError + action2.ErrorReason = "fail whale" + require.Equal(t, action2, actions[0]) +} + +// TestListActions tests some ListAction options. +// TODO(elle): cover more test cases here. +func TestListActions(t *testing.T) { + tmpDir := t.TempDir() + + db, err := NewDB(tmpDir, "test.db") + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) + + sessionID1 := [4]byte{1, 1, 1, 1} + sessionID2 := [4]byte{2, 2, 2, 2} + + actionIds := 0 + addAction := func(sessionID [4]byte) { + actionIds++ + action := &Action{ + SessionID: sessionID, + ActorName: "Autopilot", + FeatureName: fmt.Sprintf("%d", actionIds), + Trigger: "fee too low", + Intent: "increase fee", + StructuredJsonData: "{\"something\":\"nothing\"}", + RPCMethod: "UpdateChanPolicy", + RPCParamsJson: []byte("new fee"), + AttemptedAt: time.Unix(32100, 0), + State: ActionStateDone, + } + + _, err := db.AddAction(sessionID, action) + require.NoError(t, err) + } + + type action struct { + sessionID [4]byte + actionID string + } + + assertActions := func(dbActions []*Action, al []*action) { + require.Len(t, dbActions, len(al)) + for i, a := range al { + require.EqualValues( + t, a.sessionID, dbActions[i].SessionID, + ) + require.Equal(t, a.actionID, dbActions[i].FeatureName) + } + } + + addAction(sessionID1) + addAction(sessionID1) + addAction(sessionID1) + addAction(sessionID1) + addAction(sessionID2) + + actions, lastIndex, totalCount, err := db.ListActions(nil, nil) + require.NoError(t, err) + require.Len(t, actions, 5) + require.EqualValues(t, 5, lastIndex) + require.EqualValues(t, 0, totalCount) + assertActions(actions, []*action{ + {sessionID1, "1"}, + {sessionID1, "2"}, + {sessionID1, "3"}, + {sessionID1, "4"}, + {sessionID2, "5"}, + }) + + query := &ListActionsQuery{ + Reversed: true, + } + + actions, lastIndex, totalCount, err = db.ListActions(nil, query) + require.NoError(t, err) + require.Len(t, actions, 5) + require.EqualValues(t, 1, lastIndex) + require.EqualValues(t, 0, totalCount) + assertActions(actions, []*action{ + {sessionID2, "5"}, + {sessionID1, "4"}, + {sessionID1, "3"}, + {sessionID1, "2"}, + {sessionID1, "1"}, + }) + + actions, lastIndex, totalCount, err = db.ListActions( + nil, &ListActionsQuery{ + CountAll: true, + }, + ) + require.NoError(t, err) + require.Len(t, actions, 5) + require.EqualValues(t, 5, lastIndex) + require.EqualValues(t, 5, totalCount) + assertActions(actions, []*action{ + {sessionID1, "1"}, + {sessionID1, "2"}, + {sessionID1, "3"}, + {sessionID1, "4"}, + {sessionID2, "5"}, + }) + + actions, lastIndex, totalCount, err = db.ListActions( + nil, &ListActionsQuery{ + CountAll: true, + Reversed: true, + }, + ) + require.NoError(t, err) + require.Len(t, actions, 5) + require.EqualValues(t, 1, lastIndex) + require.EqualValues(t, 5, totalCount) + assertActions(actions, []*action{ + {sessionID2, "5"}, + {sessionID1, "4"}, + {sessionID1, "3"}, + {sessionID1, "2"}, + {sessionID1, "1"}, + }) + + addAction(sessionID2) + addAction(sessionID2) + addAction(sessionID1) + addAction(sessionID1) + addAction(sessionID2) + + actions, lastIndex, totalCount, err = db.ListActions(nil, nil) + require.NoError(t, err) + require.Len(t, actions, 10) + require.EqualValues(t, 10, lastIndex) + require.EqualValues(t, 0, totalCount) + assertActions(actions, []*action{ + {sessionID1, "1"}, + {sessionID1, "2"}, + {sessionID1, "3"}, + {sessionID1, "4"}, + {sessionID2, "5"}, + {sessionID2, "6"}, + {sessionID2, "7"}, + {sessionID1, "8"}, + {sessionID1, "9"}, + {sessionID2, "10"}, + }) + + actions, lastIndex, totalCount, err = db.ListActions( + nil, &ListActionsQuery{ + MaxNum: 3, + CountAll: true, + }, + ) + require.NoError(t, err) + require.Len(t, actions, 3) + require.EqualValues(t, 3, lastIndex) + require.EqualValues(t, 10, totalCount) + assertActions(actions, []*action{ + {sessionID1, "1"}, + {sessionID1, "2"}, + {sessionID1, "3"}, + }) + + actions, lastIndex, totalCount, err = db.ListActions( + nil, &ListActionsQuery{ + MaxNum: 3, + IndexOffset: 3, + }, + ) + require.NoError(t, err) + require.Len(t, actions, 3) + require.EqualValues(t, 6, lastIndex) + require.EqualValues(t, 0, totalCount) + assertActions(actions, []*action{ + {sessionID1, "4"}, + {sessionID2, "5"}, + {sessionID2, "6"}, + }) + + actions, lastIndex, totalCount, err = db.ListActions( + nil, &ListActionsQuery{ + MaxNum: 3, + IndexOffset: 3, + CountAll: true, + }, + ) + require.NoError(t, err) + require.Len(t, actions, 3) + require.EqualValues(t, 6, lastIndex) + require.EqualValues(t, 10, totalCount) + assertActions(actions, []*action{ + {sessionID1, "4"}, + {sessionID2, "5"}, + {sessionID2, "6"}, + }) +} diff --git a/firewalldb/db.go b/firewalldb/db.go new file mode 100644 index 000000000..7c862d5a4 --- /dev/null +++ b/firewalldb/db.go @@ -0,0 +1,147 @@ +package firewalldb + +import ( + "encoding/binary" + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "go.etcd.io/bbolt" +) + +const ( + // DBFilename is the default filename of the rules' database. + DBFilename = "rules.db" + + // dbFilePermission is the default permission the rules' database file + // is created with. + dbFilePermission = 0600 + + // DefaultRulesDBTimeout is the default maximum time we wait for the + // db bbolt database to be opened. If the database is already + // opened by another process, the unique lock cannot be obtained. With + // the timeout we error out after the given time instead of just + // blocking for forever. + DefaultRulesDBTimeout = 5 * time.Second +) + +var ( + // byteOrder is the default byte order we'll use for serialization + // within the database. + byteOrder = binary.BigEndian + + // ErrNoSuchKeyFound is returned when there is no key-value pair found + // for the given key. + ErrNoSuchKeyFound = fmt.Errorf("no such key found") +) + +// DB is a bolt-backed persistent store. +type DB struct { + *bbolt.DB +} + +// NewDB creates a new bolt database that can be found at the given directory. +func NewDB(dir, fileName string) (*DB, error) { + firstInit := false + path := filepath.Join(dir, fileName) + + // If the database file does not exist yet, create its directory. + if !fileExists(path) { + if err := os.MkdirAll(dir, 0700); err != nil { + return nil, err + } + firstInit = true + } + + db, err := initDB(path, firstInit) + if err != nil { + return nil, err + } + + // Attempt to sync the database's current version with the latest known + // version available. + if err := syncVersions(db); err != nil { + return nil, err + } + + return &DB{DB: db}, nil +} + +// fileExists reports whether the named file or directory exists. +func fileExists(path string) bool { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} + +// initDB initializes all the required top-level buckets for the database. +func initDB(filepath string, firstInit bool) (*bbolt.DB, error) { + db, err := bbolt.Open(filepath, dbFilePermission, &bbolt.Options{ + Timeout: DefaultRulesDBTimeout, + }) + if err == bbolt.ErrTimeout { + return nil, fmt.Errorf("error while trying to open %s: timed "+ + "out after %v when trying to obtain exclusive lock", + filepath, DefaultRulesDBTimeout) + } + if err != nil { + return nil, err + } + + err = db.Update(func(tx *bbolt.Tx) error { + if firstInit { + metadataBucket, err := tx.CreateBucketIfNotExists( + metadataBucketKey, + ) + if err != nil { + return err + } + err = setDBVersion(metadataBucket, latestDBVersion) + if err != nil { + return err + } + } + + rulesBucket, err := tx.CreateBucketIfNotExists(rulesBucketKey) + if err != nil { + return err + } + + // Delete everything under the "temp" key if such a bucket + // exists. + err = rulesBucket.DeleteBucket(tempBucketKey) + if err != nil && !errors.Is(err, bbolt.ErrBucketNotFound) { + return err + } + + actionsBucket, err := tx.CreateBucketIfNotExists( + actionsBucketKey, + ) + if err != nil { + return err + } + + _, err = actionsBucket.CreateBucketIfNotExists(actionsKey) + if err != nil { + return err + } + + _, err = actionsBucket.CreateBucketIfNotExists(actionsIndex) + if err != nil { + return err + } + + _, err = tx.CreateBucketIfNotExists(privacyBucketKey) + return err + }) + if err != nil { + return nil, err + } + + return db, nil +} diff --git a/firewalldb/kvstores.go b/firewalldb/kvstores.go new file mode 100644 index 000000000..caf44e9b7 --- /dev/null +++ b/firewalldb/kvstores.go @@ -0,0 +1,465 @@ +package firewalldb + +import ( + "context" + + "github.com/lightninglabs/lightning-terminal/session" + "go.etcd.io/bbolt" +) + +/* +The KVStores are stored in the following structure in the KV db. Note that +the `perm` and `temp` buckets are identical in structure. The only difference is +that the `temp` bucket is cleared on restart of the db. + +rules -> perm -> rule-name -> global -> {k:v} + -> sessions -> sessionID -> session-kv-store -> {k:v} + -> feature-kv-stores -> feature-name -> {k:v} + + -> temp -> rule-name -> global -> {k:v} + -> sessions -> sessionID -> session-kv-store -> {k:v} + -> feature-kv-stores -> feature-name -> {k:v} +*/ + +var ( + // rulesBucketKey is the key under which all things rule-kvstore + // related will fall. + rulesBucketKey = []byte("rules") + + // permBucketKey is a sub bucket under the rules bucket. Everything + // stored under this key is persisted across restarts. + permBucketKey = []byte("perm") + + // tempBucketKey is a sub bucket under the rules bucket. Everything + // stored under this key is cleared on restart of the db. + tempBucketKey = []byte("temp") + + // globalKVStoreBucketKey is a key under which a kv store is that will + // always be available to a specific rule regardless of which session + // or feature is being evaluated. + globalKVStoreBucketKey = []byte("global") + + // sessKVStoreBucketKey is the key under which a session wide kv store + // for the rule is stored. + sessKVStoreBucketKey = []byte("session-kv-store") + + // featureKVStoreBucketKey is the kye under which a kv store specific + // the session id and feature name is stored. + featureKVStoreBucketKey = []byte("feature-kv-store") +) + +// KVStores provides an Update and View method that will allow the caller to +// perform atomic read and write transactions on and of the key value stores +// offered the KVStoreTx. +type KVStores interface { + // Update opens a database read/write transaction and executes the + // function f with the transaction passed as a parameter. After f exits, + // if f did not error, the transaction is committed. Otherwise, if f did + // error, the transaction is rolled back. If the rollback fails, the + // original error returned by f is still returned. If the commit fails, + // the commit error is returned. + Update(f func(tx KVStoreTx) error) error + + // View opens a database read transaction and executes the function f + // with the transaction passed as a parameter. After f exits, the + // transaction is rolled back. If f errors, its error is returned, not a + // rollback error (if any occur). + View(f func(tx KVStoreTx) error) error +} + +// KVStoreTx represents a database transaction that can be used for both read +// and writes of the various different key value stores offered for the rule. +type KVStoreTx interface { + // Global returns a persisted global, rule-name indexed, kv store. A + // rule with a given name will have access to this store independent of + // session ID or feature. + Global() KVStore + + // Local returns a persisted local kv store for the rule. Depending on + // how the implementation is initialised, this will either be under the + // session ID namespace or the session ID _and_ feature name namespace. + Local() KVStore + + // GlobalTemp is similar to the Global store except that its contents + // is cleared upon restart of the database. + GlobalTemp() KVStore + + // LocalTemp is similar to the Local store except that its contents is + // cleared upon restart of the database. + LocalTemp() KVStore +} + +// KVStore is in interface representing a key value store. It allows us to +// abstract away the details of the data storage method. +type KVStore interface { + // Get fetches the value under the given key from the underlying kv + // store. If no value is found, nil is returned. + Get(ctx context.Context, key string) ([]byte, error) + + // Set sets the given key-value pair in the underlying kv store. + Set(ctx context.Context, key string, value []byte) error + + // Del deletes the value under the given key in the underlying kv store. + Del(ctx context.Context, key string) error +} + +// RulesDB can be used to initialise a new rules.KVStores. +type RulesDB interface { + GetKVStores(rule string, sessionID session.ID, feature string) KVStores +} + +// GetKVStores constructs a new rules.KVStores backed by a bbolt db. +func (db *DB) GetKVStores(rule string, sessionID session.ID, + feature string) KVStores { + + return &kvStores{ + DB: db, + ruleName: rule, + sessionID: sessionID, + featureName: feature, + } +} + +// kvStores implements the rules.KVStores interface. +type kvStores struct { + *DB + ruleName string + sessionID session.ID + featureName string +} + +// beginTx starts db transaction. The transaction will be a read or read-write +// transaction depending on the value of the `writable` parameter. +func (s *kvStores) beginTx(writable bool) (*kvStoreTx, error) { + boltTx, err := s.Begin(writable) + if err != nil { + return nil, err + } + return &kvStoreTx{ + kvStores: s, + boltTx: boltTx, + }, nil +} + +// Update opens a database read/write transaction and executes the function f +// with the transaction passed as a parameter. After f exits, if f did not +// error, the transaction is committed. Otherwise, if f did error, the +// transaction is rolled back. If the rollback fails, the original error +// returned by f is still returned. If the commit fails, the commit error is +// returned. +// +// NOTE: this is part of the KVStores interface. +func (s *kvStores) Update(f func(tx KVStoreTx) error) error { + tx, err := s.beginTx(true) + if err != nil { + return err + } + + // Make sure the transaction rolls back in the event of a panic. + defer func() { + if tx != nil { + _ = tx.boltTx.Rollback() + } + }() + + err = f(tx) + if err != nil { + // Want to return the original error, not a rollback error if + // any occur. + _ = tx.boltTx.Rollback() + return err + } + + return tx.boltTx.Commit() +} + +// View opens a database read transaction and executes the function f with the +// transaction passed as a parameter. After f exits, the transaction is rolled +// back. If f errors, its error is returned, not a rollback error (if any +// occur). +// +// NOTE: this is part of the KVStores interface. +func (s *kvStores) View(f func(tx KVStoreTx) error) error { + tx, err := s.beginTx(false) + if err != nil { + return err + } + + // Make sure the transaction rolls back in the event of a panic. + defer func() { + if tx != nil { + _ = tx.boltTx.Rollback() + } + }() + + err = f(tx) + rollbackErr := tx.boltTx.Rollback() + if err != nil { + return err + } + + if rollbackErr != nil { + return rollbackErr + } + return nil +} + +// getBucketFunc defines the signature of the bucket creation/fetching function +// required by kvStoreTx. If create is true, then all the bucket (and all +// buckets leading up to the bucket) should be created if they do not already +// exist. Otherwise, if the bucket or any leading up to it does not yet exist +// then nil is returned. +type getBucketFunc func(tx *bbolt.Tx, create bool) (*bbolt.Bucket, error) + +// kvStoreTx represents an open transaction of kvStores. +// This implements the KVStoreTX interface. +type kvStoreTx struct { + boltTx *bbolt.Tx + getBucket getBucketFunc + + *kvStores +} + +// Global gives the caller access to the global kv store of the rule. +// +// NOTE: this is part of the rules.KVStoreTx interface. +func (tx *kvStoreTx) Global() KVStore { + return &kvStoreTx{ + kvStores: tx.kvStores, + boltTx: tx.boltTx, + getBucket: getGlobalRuleBucket(true, tx.ruleName), + } +} + +// Local gives the caller access to the local kv store of the rule. This will +// either be a session wide kv store or a feature specific one depending on +// how the kv store was initialised. +// +// NOTE: this is part of the KVStoreTx interface. +func (tx *kvStoreTx) Local() KVStore { + fn := getSessionRuleBucket(true, tx.ruleName, tx.sessionID) + if tx.featureName != "" { + fn = getSessionFeatureRuleBucket( + true, tx.ruleName, tx.sessionID, tx.featureName, + ) + } + + return &kvStoreTx{ + kvStores: tx.kvStores, + boltTx: tx.boltTx, + getBucket: fn, + } +} + +// GlobalTemp gives the caller access to the temporary global kv store of the +// rule. +// +// NOTE: this is part of the KVStoreTx interface. +func (tx *kvStoreTx) GlobalTemp() KVStore { + return &kvStoreTx{ + kvStores: tx.kvStores, + boltTx: tx.boltTx, + getBucket: getGlobalRuleBucket(false, tx.ruleName), + } +} + +// LocalTemp gives the caller access to the temporary local kv store of the +// rule. +// +// NOTE: this is part of the KVStoreTx interface. +func (tx *kvStoreTx) LocalTemp() KVStore { + fn := getSessionRuleBucket(true, tx.ruleName, tx.sessionID) + if tx.featureName != "" { + fn = getSessionFeatureRuleBucket( + false, tx.ruleName, tx.sessionID, tx.featureName, + ) + } + + return &kvStoreTx{ + kvStores: tx.kvStores, + boltTx: tx.boltTx, + getBucket: fn, + } +} + +// Get fetches the value under the given key from the underlying kv store. +// If no value is found, nil is returned. +// +// NOTE: this is part of the KVStore interface. +func (tx *kvStoreTx) Get(_ context.Context, key string) ([]byte, error) { + bucket, err := tx.getBucket(tx.boltTx, false) + if err != nil { + return nil, err + } + if bucket == nil { + return nil, nil + } + + return bucket.Get([]byte(key)), nil +} + +// Set sets the given key-value pair in the underlying kv store. +// +// NOTE: this is part of the KVStore interface. +func (tx *kvStoreTx) Set(_ context.Context, key string, value []byte) error { + bucket, err := tx.getBucket(tx.boltTx, true) + if err != nil { + return err + } + + return bucket.Put([]byte(key), value) +} + +// Del deletes the value under the given key in the underlying kv store. +// +// NOTE: this is part of the .KVStore interface. +func (tx *kvStoreTx) Del(_ context.Context, key string) error { + bucket, err := tx.getBucket(tx.boltTx, false) + if err != nil { + return err + } + if bucket == nil { + return nil + } + + return bucket.Delete([]byte(key)) +} + +func getMainBucket(tx *bbolt.Tx, create, perm bool) (*bbolt.Bucket, error) { + mainBucket, err := getBucket(tx, rulesBucketKey) + if err != nil { + return nil, err + } + + key := tempBucketKey + if perm { + key = permBucketKey + } + + if create { + return mainBucket.CreateBucketIfNotExists(key) + } + + return mainBucket.Bucket(key), nil +} + +// getRuleBucket returns a function that can be used to access the bucket for +// a given rule name. The `perm` param determines if the temporary or permanent +// store is used. +func getRuleBucket(perm bool, ruleName string) getBucketFunc { + return func(tx *bbolt.Tx, create bool) (*bbolt.Bucket, error) { + mainBucket, err := getMainBucket(tx, create, perm) + if err != nil { + return nil, err + } + + if create { + return mainBucket.CreateBucketIfNotExists( + []byte(ruleName), + ) + } else if mainBucket == nil { + return nil, nil + } + + return mainBucket.Bucket([]byte(ruleName)), nil + } +} + +// getGlobalRuleBucket returns a function that can be used to access the global +// kv store of the given rule name. The `perm` param determines if the temporary +// or permanent store is used. +func getGlobalRuleBucket(perm bool, ruleName string) getBucketFunc { + return func(tx *bbolt.Tx, create bool) (*bbolt.Bucket, error) { + ruleBucket, err := getRuleBucket(perm, ruleName)(tx, create) + if err != nil { + return nil, err + } + + if ruleBucket == nil && !create { + return nil, nil + } + + if create { + return ruleBucket.CreateBucketIfNotExists( + globalKVStoreBucketKey, + ) + } + + return ruleBucket.Bucket(globalKVStoreBucketKey), nil + } +} + +// getSessionRuleBucket returns a function that can be used to fetch the +// bucket under which a kv store for a specific rule-name and session ID is +// stored. The `perm` param determines if the temporary or permanent store is +// used. +func getSessionRuleBucket(perm bool, ruleName string, + sessionID session.ID) getBucketFunc { + + return func(tx *bbolt.Tx, create bool) (*bbolt.Bucket, error) { + ruleBucket, err := getRuleBucket(perm, ruleName)(tx, create) + if err != nil { + return nil, err + } + + if ruleBucket == nil && !create { + return nil, nil + } + + if create { + sessBucket, err := ruleBucket.CreateBucketIfNotExists( + sessKVStoreBucketKey, + ) + if err != nil { + return nil, err + } + + return sessBucket.CreateBucketIfNotExists(sessionID[:]) + } + + sessBucket := ruleBucket.Bucket(sessKVStoreBucketKey) + if sessBucket == nil { + return nil, nil + } + return sessBucket.Bucket(sessionID[:]), nil + } +} + +// getSessionFeatureRuleBucket returns a function that can be used to fetch the +// bucket under which a kv store for a specific rule-name, session ID and +// feature name is stored. The `perm` param determines if the temporary or +// permanent store is used. +func getSessionFeatureRuleBucket(perm bool, ruleName string, + sessionID session.ID, featureName string) getBucketFunc { + + return func(tx *bbolt.Tx, create bool) (*bbolt.Bucket, error) { + sessBucket, err := getSessionRuleBucket( + perm, ruleName, sessionID)(tx, create) + if err != nil { + return nil, err + } + + if sessBucket == nil && !create { + return nil, nil + } + + if create { + featureBucket, err := sessBucket.CreateBucketIfNotExists( + featureKVStoreBucketKey, + ) + if err != nil { + return nil, err + } + + return featureBucket.CreateBucketIfNotExists( + []byte(featureName), + ) + } + + featureBucket := sessBucket.Bucket(featureKVStoreBucketKey) + if featureBucket == nil { + return nil, nil + } + return featureBucket.Bucket([]byte(featureName)), nil + } +} diff --git a/firewalldb/kvstores_test.go b/firewalldb/kvstores_test.go new file mode 100644 index 000000000..dc5076439 --- /dev/null +++ b/firewalldb/kvstores_test.go @@ -0,0 +1,327 @@ +package firewalldb + +import ( + "bytes" + "context" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestKVStoreTxs tests that the `Update` and `View` functions correctly provide +// atomic access to the db. If anything fails in the middle of an `Update` +// function, then all the changes prior should be rolled back. +func TestKVStoreTxs(t *testing.T) { + ctx := context.Background() + tmpDir := t.TempDir() + + db, err := NewDB(tmpDir, "test.db") + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) + + store := db.GetKVStores("AutoFees", [4]byte{1, 1, 1, 1}, "auto-fees") + + // Test that if an action fails midway through the transaction, then + // it is rolled back. + err = store.Update(func(tx KVStoreTx) error { + err := tx.Global().Set(ctx, "test", []byte{1}) + if err != nil { + return err + } + + b, err := tx.Global().Get(ctx, "test") + if err != nil { + return err + } + require.True(t, bytes.Equal(b, []byte{1})) + + // Now return an error. + return fmt.Errorf("random error") + }) + require.Error(t, err) + + var v []byte + err = store.View(func(tx KVStoreTx) error { + b, err := tx.Global().Get(ctx, "test") + if err != nil { + return err + } + v = b + return nil + }) + require.NoError(t, err) + require.Nil(t, v) +} + +// TestTempAndPermStores tests that the kv stores stored under the `temp` bucket +// are properly deleted and re-initialised upon restart but that anything under +// the `perm` bucket is retained. +func TestTempAndPermStores(t *testing.T) { + ctx := context.Background() + tmpDir := t.TempDir() + + db, err := NewDB(tmpDir, "test.db") + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) + + store := db.GetKVStores("test-rule", [4]byte{1, 1, 1, 1}, "auto-fees") + + err = store.Update(func(tx KVStoreTx) error { + // Set an item in the temp store. + err := tx.LocalTemp().Set(ctx, "test", []byte{4, 3, 2}) + if err != nil { + return err + } + + // Set an item in the perm store. + return tx.Local().Set(ctx, "test", []byte{6, 5, 4}) + }) + require.NoError(t, err) + + // Make sure that the newly added items are properly reflected _before_ + // restart. + var ( + v1 []byte + v2 []byte + ) + err = store.View(func(tx KVStoreTx) error { + b, err := tx.LocalTemp().Get(ctx, "test") + if err != nil { + return err + } + v1 = b + + b, err = tx.Local().Get(ctx, "test") + if err != nil { + return err + } + v2 = b + return nil + }) + require.NoError(t, err) + require.True(t, bytes.Equal(v1, []byte{4, 3, 2})) + require.True(t, bytes.Equal(v2, []byte{6, 5, 4})) + + // Close the db. + require.NoError(t, db.Close()) + + // Restart it. + db, err = NewDB(tmpDir, "test.db") + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + _ = os.RemoveAll(tmpDir) + }) + store = db.GetKVStores("test-rule", [4]byte{1, 1, 1, 1}, "auto-fees") + + // The temp store should no longer have the stored value but the perm + // store should . + err = store.View(func(tx KVStoreTx) error { + b, err := tx.LocalTemp().Get(ctx, "test") + if err != nil { + return err + } + v1 = b + + b, err = tx.Local().Get(ctx, "test") + if err != nil { + return err + } + v2 = b + return nil + }) + require.NoError(t, err) + require.Nil(t, v1) + require.True(t, bytes.Equal(v2, []byte{6, 5, 4})) +} + +// TestKVStoreNameSpaces tests that the various name spaces are used correctly. +func TestKVStoreNameSpaces(t *testing.T) { + ctx := context.Background() + tmpDir := t.TempDir() + + db, err := NewDB(tmpDir, "test.db") + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) + + var ( + sessionID1 = [4]byte{1, 1, 1, 1} + sessionID2 = [4]byte{2, 2, 2, 2} + ) + + // Two DBs for same session but different features. + rulesDB1 := db.GetKVStores("test-rule", sessionID1, "auto-fees") + rulesDB2 := db.GetKVStores("test-rule", sessionID1, "re-balance") + + // The third DB is for the same rule but a different session. It is + // for the same feature as db 2. + rulesDB3 := db.GetKVStores("test-rule", sessionID2, "re-balance") + + // Test that the three ruleDBs share the same global space. + err = rulesDB1.Update(func(tx KVStoreTx) error { + return tx.Global().Set( + ctx, "test-global", []byte("global thing!"), + ) + }) + require.NoError(t, err) + + err = rulesDB2.Update(func(tx KVStoreTx) error { + return tx.Global().Set( + ctx, "test-global", []byte("different global thing!"), + ) + }) + require.NoError(t, err) + + err = rulesDB3.Update(func(tx KVStoreTx) error { + return tx.Global().Set( + ctx, "test-global", []byte("yet another global thing"), + ) + }) + require.NoError(t, err) + + var v []byte + err = rulesDB1.View(func(tx KVStoreTx) error { + b, err := tx.Global().Get(ctx, "test-global") + if err != nil { + return err + } + v = b + return nil + }) + require.NoError(t, err) + require.True(t, bytes.Equal(v, []byte("yet another global thing"))) + + err = rulesDB2.View(func(tx KVStoreTx) error { + b, err := tx.Global().Get(ctx, "test-global") + if err != nil { + return err + } + v = b + return nil + }) + require.NoError(t, err) + require.True(t, bytes.Equal(v, []byte("yet another global thing"))) + + err = rulesDB3.View(func(tx KVStoreTx) error { + b, err := tx.Global().Get(ctx, "test-global") + if err != nil { + return err + } + v = b + return nil + }) + require.NoError(t, err) + require.True(t, bytes.Equal(v, []byte("yet another global thing"))) + + // Test that the feature space is not shared by any of the dbs. + err = rulesDB1.Update(func(tx KVStoreTx) error { + return tx.Local().Set(ctx, "count", []byte("1")) + }) + require.NoError(t, err) + + err = rulesDB2.Update(func(tx KVStoreTx) error { + return tx.Local().Set(ctx, "count", []byte("2")) + }) + require.NoError(t, err) + + err = rulesDB3.Update(func(tx KVStoreTx) error { + return tx.Local().Set(ctx, "count", []byte("3")) + }) + require.NoError(t, err) + + err = rulesDB1.View(func(tx KVStoreTx) error { + b, err := tx.Local().Get(ctx, "count") + if err != nil { + return err + } + v = b + return nil + }) + require.NoError(t, err) + require.True(t, bytes.Equal(v, []byte("1"))) + + err = rulesDB2.View(func(tx KVStoreTx) error { + b, err := tx.Local().Get(ctx, "count") + if err != nil { + return err + } + v = b + return nil + }) + require.NoError(t, err) + require.True(t, bytes.Equal(v, []byte("2"))) + + err = rulesDB3.View(func(tx KVStoreTx) error { + b, err := tx.Local().Get(ctx, "count") + if err != nil { + return err + } + v = b + return nil + }) + require.NoError(t, err) + require.True(t, bytes.Equal(v, []byte("3"))) + + // Test that the session space is shared by the first two dbs but not + // the third. To do this, we re-init the DB's but leave the feature + // names out. This way, we will access the session storage. + rulesDB1 = db.GetKVStores("test-rule", sessionID1, "") + rulesDB2 = db.GetKVStores("test-rule", sessionID1, "") + rulesDB3 = db.GetKVStores("test-rule", sessionID2, "") + + err = rulesDB1.Update(func(tx KVStoreTx) error { + return tx.Local().Set(ctx, "test", []byte("thing 1")) + }) + require.NoError(t, err) + + err = rulesDB2.Update(func(tx KVStoreTx) error { + return tx.Local().Set(ctx, "test", []byte("thing 2")) + }) + require.NoError(t, err) + + err = rulesDB3.Update(func(tx KVStoreTx) error { + return tx.Local().Set(ctx, "test", []byte("thing 3")) + }) + require.NoError(t, err) + + err = rulesDB1.View(func(tx KVStoreTx) error { + b, err := tx.Local().Get(ctx, "test") + if err != nil { + return err + } + v = b + return nil + }) + require.NoError(t, err) + require.True(t, bytes.Equal(v, []byte("thing 2"))) + + err = rulesDB2.View(func(tx KVStoreTx) error { + b, err := tx.Local().Get(ctx, "test") + if err != nil { + return err + } + v = b + return nil + }) + require.NoError(t, err) + require.True(t, bytes.Equal(v, []byte("thing 2"))) + + err = rulesDB3.View(func(tx KVStoreTx) error { + b, err := tx.Local().Get(ctx, "test") + if err != nil { + return err + } + v = b + return nil + }) + require.NoError(t, err) + require.True(t, bytes.Equal(v, []byte("thing 3"))) +} diff --git a/firewalldb/log.go b/firewalldb/log.go new file mode 100644 index 000000000..48fbb13af --- /dev/null +++ b/firewalldb/log.go @@ -0,0 +1,25 @@ +package firewalldb + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +const Subsystem = "FWDB" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/firewalldb/metadata.go b/firewalldb/metadata.go new file mode 100644 index 000000000..ff9438e9f --- /dev/null +++ b/firewalldb/metadata.go @@ -0,0 +1,121 @@ +package firewalldb + +import ( + "errors" + "fmt" + + "go.etcd.io/bbolt" +) + +// migration is a function which takes a prior outdated version of the database +// instance and mutates the key/bucket structure to arrive at a more up-to-date +// version of the database. +type migration func(tx *bbolt.Tx) error + +var ( + // metadataBucketKey stores all the metadata concerning the state of the + // database. + metadataBucketKey = []byte("metadata") + + // dbVersionKey is the key used for storing/retrieving the current + // database version. + dbVersionKey = []byte("version") + + // ErrDBReversion is returned when detecting an attempt to revert to a + // prior database version. + ErrDBReversion = errors.New("cannot revert to prior version") + + // dbVersions is storing all versions of database. If the current + // version of the database doesn't match the latest version this list + // will be used for retrieving all migration function that are need to + // apply to the current db. + dbVersions []migration + + latestDBVersion = uint32(len(dbVersions)) +) + +// getDBVersion retrieves the current database version. +func getDBVersion(bucket *bbolt.Bucket) (uint32, error) { + versionBytes := bucket.Get(dbVersionKey) + if versionBytes == nil { + return 0, errors.New("database version not found") + } + return byteOrder.Uint32(versionBytes), nil +} + +// setDBVersion updates the current database version. +func setDBVersion(bucket *bbolt.Bucket, version uint32) error { + var b [4]byte + byteOrder.PutUint32(b[:], version) + return bucket.Put(dbVersionKey, b[:]) +} + +// getBucket retrieves the bucket with the given key. +func getBucket(tx *bbolt.Tx, key []byte) (*bbolt.Bucket, error) { + bucket := tx.Bucket(key) + if bucket == nil { + return nil, fmt.Errorf("bucket \"%v\" does not exist", + string(key)) + } + return bucket, nil +} + +// syncVersions function is used for safe db version synchronization. It +// applies migration functions to the current database and recovers the +// previous state of db if at least one error/panic appeared during migration. +func syncVersions(db *bbolt.DB) error { + var currentVersion uint32 + err := db.View(func(tx *bbolt.Tx) error { + metadata, err := getBucket(tx, metadataBucketKey) + if err != nil { + return err + } + currentVersion, err = getDBVersion(metadata) + return err + }) + if err != nil { + return err + } + + log.Infof("Checking for schema update: latest_version=%v, "+ + "db_version=%v", latestDBVersion, currentVersion) + + switch { + // If the database reports a higher version that we are aware of, the + // user is probably trying to revert to a prior version of lnd. We fail + // here to prevent reversions and unintended corruption. + case currentVersion > latestDBVersion: + log.Errorf("Refusing to revert from db_version=%d to "+ + "lower version=%d", currentVersion, + latestDBVersion) + + return ErrDBReversion + + // If the current database version matches the latest version number, + // then we don't need to perform any migrations. + case currentVersion == latestDBVersion: + return nil + } + + log.Infof("Performing database schema migration") + + // Otherwise, we execute the migrations serially within a single + // database transaction to ensure the migration is atomic. + return db.Update(func(tx *bbolt.Tx) error { + for v := currentVersion; v < latestDBVersion; v++ { + log.Infof("Applying migration #%v", v+1) + + migration := dbVersions[v] + if err := migration(tx); err != nil { + log.Infof("Unable to apply migration #%v", v+1) + return err + } + } + + metadata, err := getBucket(tx, metadataBucketKey) + if err != nil { + return err + } + return setDBVersion(metadata, latestDBVersion) + }) +} diff --git a/firewalldb/privacy_mapper.go b/firewalldb/privacy_mapper.go new file mode 100644 index 000000000..7d96060fc --- /dev/null +++ b/firewalldb/privacy_mapper.go @@ -0,0 +1,462 @@ +package firewalldb + +import ( + "crypto/rand" + "encoding/binary" + "encoding/hex" + "fmt" + "math/big" + "strconv" + "strings" + + "github.com/lightninglabs/lightning-terminal/session" + "go.etcd.io/bbolt" +) + +/* + The PrivacyMapper data is stored in the following structure in the db: + + privacy -> session id -> real-to-pseudo -> {k:v} + -> pseudo-to-real -> {k:v} +*/ + +const ( + txidStringLen = 64 +) + +var ( + privacyBucketKey = []byte("privacy") + realToPseudoKey = []byte("real-to-pseudo") + pseudoToRealKey = []byte("pseudo-to-real") + + pseudoStrAlphabet = []rune("abcdef0123456789") + pseudoStrAlphabetLen = len(pseudoStrAlphabet) +) + +// NewPrivacyMapDB is a function type that takes a session ID and uses it to +// construct a new PrivacyMapDB. +type NewPrivacyMapDB func(sessionID session.ID) PrivacyMapDB + +// PrivacyDB constructs a PrivacyMapDB that will be indexed under the given +// sessionID key. +func (db *DB) PrivacyDB(sessionID session.ID) PrivacyMapDB { + return &privacyMapDB{ + DB: db, + sessionID: sessionID, + } +} + +// PrivacyMapDB provides an Update and View method that will allow the caller +// to perform atomic read and write transactions defined by PrivacyMapTx on the +// underlying DB. +type PrivacyMapDB interface { + // Update opens a database read/write transaction and executes the + // function f with the transaction passed as a parameter. After f exits, + // if f did not error, the transaction is committed. Otherwise, if f did + // error, the transaction is rolled back. If the rollback fails, the + // original error returned by f is still returned. If the commit fails, + // the commit error is returned. + Update(f func(tx PrivacyMapTx) error) error + + // View opens a database read transaction and executes the function f + // with the transaction passed as a parameter. After f exits, the + // transaction is rolled back. If f errors, its error is returned, not a + // rollback error (if any occur). + View(f func(tx PrivacyMapTx) error) error +} + +// PrivacyMapTx represents a db that can be used to create, store and fetch +// real-pseudo pairs. +type PrivacyMapTx interface { + // NewPair persists a new real-pseudo pair. + NewPair(real, pseudo string) error + + // PseudoToReal returns the real value associated with the given pseudo + // value. If no such pair is found, then ErrNoSuchKeyFound is returned. + PseudoToReal(pseudo string) (string, error) + + // RealToPseudo returns the pseudo value associated with the given real + // value. If no such pair is found, then ErrNoSuchKeyFound is returned. + RealToPseudo(real string) (string, error) +} + +// privacyMapDB is an implementation of PrivacyMapDB. +type privacyMapDB struct { + *DB + sessionID session.ID +} + +// beginTx starts db transaction. The transaction will be a read or read-write +// transaction depending on the value of the `writable` parameter. +func (p *privacyMapDB) beginTx(writable bool) (*privacyMapTx, error) { + boltTx, err := p.Begin(writable) + if err != nil { + return nil, err + } + return &privacyMapTx{ + privacyMapDB: p, + boltTx: boltTx, + }, nil +} + +// Update opens a database read/write transaction and executes the function f +// with the transaction passed as a parameter. After f exits, if f did not +// error, the transaction is committed. Otherwise, if f did error, the +// transaction is rolled back. If the rollback fails, the original error +// returned by f is still returned. If the commit fails, the commit error is +// returned. +// +// NOTE: this is part of the PrivacyMapDB interface. +func (p *privacyMapDB) Update(f func(tx PrivacyMapTx) error) error { + tx, err := p.beginTx(true) + if err != nil { + return err + } + + // Make sure the transaction rolls back in the event of a panic. + defer func() { + if tx != nil { + _ = tx.boltTx.Rollback() + } + }() + + err = f(tx) + if err != nil { + // Want to return the original error, not a rollback error if + // any occur. + _ = tx.boltTx.Rollback() + return err + } + + return tx.boltTx.Commit() +} + +// View opens a database read transaction and executes the function f with the +// transaction passed as a parameter. After f exits, the transaction is rolled +// back. If f errors, its error is returned, not a rollback error (if any +// occur). +// +// NOTE: this is part of the PrivacyMapDB interface. +func (p *privacyMapDB) View(f func(tx PrivacyMapTx) error) error { + tx, err := p.beginTx(false) + if err != nil { + return err + } + + // Make sure the transaction rolls back in the event of a panic. + defer func() { + if tx != nil { + _ = tx.boltTx.Rollback() + } + }() + + err = f(tx) + rollbackErr := tx.boltTx.Rollback() + if err != nil { + return err + } + + if rollbackErr != nil { + return rollbackErr + } + return nil +} + +// privacyMapTx is an implementation of PrivacyMapTx. +type privacyMapTx struct { + *privacyMapDB + boltTx *bbolt.Tx +} + +// NewPair inserts a new real-pseudo pair into the db. +func (p *privacyMapTx) NewPair(real, pseudo string) error { + privacyBucket, err := getBucket(p.boltTx, privacyBucketKey) + if err != nil { + return err + } + + sessBucket, err := privacyBucket.CreateBucketIfNotExists(p.sessionID[:]) + if err != nil { + return err + } + + realToPseudoBucket, err := sessBucket.CreateBucketIfNotExists( + realToPseudoKey, + ) + if err != nil { + return err + } + + pseudoToRealBucket, err := sessBucket.CreateBucketIfNotExists( + pseudoToRealKey, + ) + if err != nil { + return err + } + + err = realToPseudoBucket.Put([]byte(real), []byte(pseudo)) + if err != nil { + return err + } + + return pseudoToRealBucket.Put([]byte(pseudo), []byte(real)) +} + +// PseudoToReal will check the db to see if the given pseudo key exists. If +// it does then the real value is returned, else an error is returned. +func (p *privacyMapTx) PseudoToReal(pseudo string) (string, error) { + privacyBucket, err := getBucket(p.boltTx, privacyBucketKey) + if err != nil { + return "", err + } + + sessBucket := privacyBucket.Bucket(p.sessionID[:]) + if sessBucket == nil { + return "", ErrNoSuchKeyFound + } + + pseudoToRealBucket := sessBucket.Bucket(pseudoToRealKey) + if pseudoToRealBucket == nil { + return "", ErrNoSuchKeyFound + } + + real := pseudoToRealBucket.Get([]byte(pseudo)) + if len(real) == 0 { + return "", ErrNoSuchKeyFound + } + + return string(real), nil +} + +// RealToPseudo will check the db to see if the given real key exists. If +// it does then the pseudo value is returned, else an error is returned. +func (p *privacyMapTx) RealToPseudo(real string) (string, error) { + privacyBucket, err := getBucket(p.boltTx, privacyBucketKey) + if err != nil { + return "", err + } + + sessBucket := privacyBucket.Bucket(p.sessionID[:]) + if sessBucket == nil { + return "", ErrNoSuchKeyFound + } + + realToPseudoBucket := sessBucket.Bucket(realToPseudoKey) + if realToPseudoBucket == nil { + return "", ErrNoSuchKeyFound + } + + pseudo := realToPseudoBucket.Get([]byte(real)) + if len(pseudo) == 0 { + return "", ErrNoSuchKeyFound + } + + return string(pseudo), nil +} + +func HideString(tx PrivacyMapTx, real string) (string, error) { + pseudo, err := tx.RealToPseudo(real) + if err != nil && err != ErrNoSuchKeyFound { + return "", err + } + if err == nil { + return pseudo, nil + } + + pseudo, err = NewPseudoStr(len(real)) + if err != nil { + return "", err + } + + if err = tx.NewPair(real, pseudo); err != nil { + return "", err + } + + return pseudo, nil +} + +func NewPseudoStr(n int) (string, error) { + var max big.Int + max.SetUint64(uint64(pseudoStrAlphabetLen)) + + b := make([]rune, n) + for i := range b { + index, err := rand.Int(rand.Reader, &max) + if err != nil { + return "", err + } + + b[i] = pseudoStrAlphabet[index.Uint64()] + } + + return string(b), nil +} + +func RevealString(tx PrivacyMapTx, pseudo string) (string, error) { + if pseudo == "" { + return pseudo, nil + } + + return tx.PseudoToReal(pseudo) +} + +func HideUint64(tx PrivacyMapTx, real uint64) (uint64, error) { + str := Uint64ToStr(real) + pseudo, err := tx.RealToPseudo(str) + if err != nil && err != ErrNoSuchKeyFound { + return 0, err + } + if err == nil { + return StrToUint64(pseudo) + } + + pseudoUint64, pseudoUint64Str := NewPseudoUint64() + if err := tx.NewPair(str, pseudoUint64Str); err != nil { + return 0, err + } + + return pseudoUint64, nil +} + +func RevealUint64(tx PrivacyMapTx, pseudo uint64) (uint64, error) { + if pseudo == 0 { + return 0, nil + } + + real, err := tx.PseudoToReal(Uint64ToStr(pseudo)) + if err != nil { + return 0, err + } + + return StrToUint64(real) +} + +func HideChanPoint(tx PrivacyMapTx, txid string, index uint32) (string, + uint32, error) { + + cp := fmt.Sprintf("%s:%d", txid, index) + pseudo, err := tx.RealToPseudo(cp) + if err != nil && err != ErrNoSuchKeyFound { + return "", 0, err + } + if err == nil { + return decodeChannelPoint(pseudo) + } + + newCp, err := NewPseudoChanPoint() + if err != nil { + return "", 0, err + } + + if err := tx.NewPair(cp, newCp); err != nil { + return "", 0, err + } + + return decodeChannelPoint(newCp) +} + +func NewPseudoChanPoint() (string, error) { + pseudoTXID, err := NewPseudoStr(txidStringLen) + if err != nil { + return "", err + } + + pseudoIndex := NewPseudoUint32() + return fmt.Sprintf("%s:%d", pseudoTXID, pseudoIndex), nil +} + +func RevealChanPoint(tx PrivacyMapTx, txid string, index uint32) (string, + uint32, error) { + + fakePoint := fmt.Sprintf("%s:%d", txid, index) + real, err := tx.PseudoToReal(fakePoint) + if err != nil { + return "", 0, err + } + + return decodeChannelPoint(real) +} + +func NewPseudoUint32() uint32 { + b := make([]byte, 4) + _, _ = rand.Read(b) + + return binary.BigEndian.Uint32(b) +} + +func HideChanPointStr(tx PrivacyMapTx, cp string) (string, error) { + txid, index, err := decodeChannelPoint(cp) + if err != nil { + return "", err + } + + newTxid, newIndex, err := HideChanPoint(tx, txid, index) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s:%d", newTxid, newIndex), nil +} + +func HideBytes(tx PrivacyMapTx, realBytes []byte) ([]byte, error) { + real := hex.EncodeToString(realBytes) + + pseudo, err := HideString(tx, real) + if err != nil { + return nil, err + } + + return hex.DecodeString(pseudo) +} + +func RevealBytes(tx PrivacyMapTx, pseudoBytes []byte) ([]byte, error) { + if pseudoBytes == nil { + return nil, nil + } + + pseudo := hex.EncodeToString(pseudoBytes) + pseudo, err := RevealString(tx, pseudo) + if err != nil { + return nil, err + } + + return hex.DecodeString(pseudo) +} + +func NewPseudoUint64() (uint64, string) { + b := make([]byte, 8) + _, _ = rand.Read(b) + + i := binary.BigEndian.Uint64(b) + + return i, hex.EncodeToString(b) +} + +func Uint64ToStr(i uint64) string { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, i) + return hex.EncodeToString(b) +} + +func StrToUint64(s string) (uint64, error) { + b, err := hex.DecodeString(s) + if err != nil { + return 0, err + } + + return binary.BigEndian.Uint64(b), nil +} + +func decodeChannelPoint(cp string) (string, uint32, error) { + parts := strings.Split(cp, ":") + if len(parts) != 2 { + return "", 0, fmt.Errorf("bad channel point encoding") + } + + index, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return "", 0, err + } + + return parts[0], uint32(index), nil +} diff --git a/firewalldb/privacy_mapper_test.go b/firewalldb/privacy_mapper_test.go new file mode 100644 index 000000000..827d42c42 --- /dev/null +++ b/firewalldb/privacy_mapper_test.go @@ -0,0 +1,103 @@ +package firewalldb + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestPrivacyMapStorage tests the privacy mapper CRUD logic. +func TestPrivacyMapStorage(t *testing.T) { + tmpDir := t.TempDir() + db, err := NewDB(tmpDir, "test.db") + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) + + pdb1 := db.PrivacyDB([4]byte{1, 1, 1, 1}) + + _ = pdb1.Update(func(tx PrivacyMapTx) error { + _, err = tx.RealToPseudo("real") + require.ErrorIs(t, err, ErrNoSuchKeyFound) + + _, err = tx.PseudoToReal("pseudo") + require.ErrorIs(t, err, ErrNoSuchKeyFound) + + err = tx.NewPair("real", "pseudo") + require.NoError(t, err) + + pseudo, err := tx.RealToPseudo("real") + require.NoError(t, err) + require.Equal(t, "pseudo", pseudo) + + real, err := tx.PseudoToReal("pseudo") + require.NoError(t, err) + require.Equal(t, "real", real) + + return nil + }) + + pdb2 := db.PrivacyDB([4]byte{2, 2, 2, 2}) + + _ = pdb2.Update(func(tx PrivacyMapTx) error { + _, err = tx.RealToPseudo("real") + require.ErrorIs(t, err, ErrNoSuchKeyFound) + + _, err = tx.PseudoToReal("pseudo") + require.ErrorIs(t, err, ErrNoSuchKeyFound) + + err = tx.NewPair("real 2", "pseudo 2") + require.NoError(t, err) + + pseudo, err := tx.RealToPseudo("real 2") + require.NoError(t, err) + require.Equal(t, "pseudo 2", pseudo) + + real, err := tx.PseudoToReal("pseudo 2") + require.NoError(t, err) + require.Equal(t, "real 2", real) + + return nil + }) +} + +// TestPrivacyMapTxs tests that the `Update` and `View` functions correctly +// provide atomic access to the db. If anything fails in the middle of an +// `Update` function, then all the changes prior should be rolled back. +func TestPrivacyMapTxs(t *testing.T) { + tmpDir := t.TempDir() + db, err := NewDB(tmpDir, "test.db") + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) + + pdb1 := db.PrivacyDB([4]byte{1, 1, 1, 1}) + + // Test that if an action fails midway through the transaction, then + // it is rolled back. + err = pdb1.Update(func(tx PrivacyMapTx) error { + err := tx.NewPair("real", "pseudo") + if err != nil { + return err + } + + p, err := tx.RealToPseudo("real") + if err != nil { + return err + } + require.Equal(t, "pseudo", p) + + // Now return an error. + return fmt.Errorf("random error") + }) + require.Error(t, err) + + err = pdb1.View(func(tx PrivacyMapTx) error { + _, err := tx.RealToPseudo("real") + return err + }) + require.ErrorIs(t, err, ErrNoSuchKeyFound) +} diff --git a/go.mod b/go.mod index 7d882f689..d56fa6359 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,14 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/btcsuite/btcwallet/walletdb v1.4.0 github.com/go-errors/errors v1.0.1 + github.com/golang/protobuf v1.5.2 github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 github.com/improbable-eng/grpc-web v0.12.0 github.com/jessevdk/go-flags v1.4.0 github.com/lightninglabs/aperture v0.1.18-beta github.com/lightninglabs/faraday v0.2.9-alpha github.com/lightninglabs/lightning-node-connect v0.1.12-alpha + github.com/lightninglabs/lightning-terminal/autopilotserverrpc v0.0.1 github.com/lightninglabs/lndclient v0.15.4-0 github.com/lightninglabs/loop v0.20.2-beta github.com/lightninglabs/loop/swapserverrpc v1.0.3 @@ -23,7 +25,8 @@ require ( github.com/lightningnetwork/lnd v0.15.5-beta github.com/lightningnetwork/lnd/cert v1.1.1 github.com/lightningnetwork/lnd/kvdb v1.3.1 - github.com/lightningnetwork/lnd/tlv v1.0.3 + github.com/lightningnetwork/lnd/tlv v1.1.0 + github.com/lightningnetwork/lnd/tor v1.0.1 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76 github.com/stretchr/testify v1.8.0 @@ -33,7 +36,7 @@ require ( golang.org/x/net v0.0.0-20211216030914-fe4d6282115f golang.org/x/sync v0.0.0-20210220032951-036812b2e83c google.golang.org/grpc v1.39.0 - google.golang.org/protobuf v1.27.1 + google.golang.org/protobuf v1.28.1 gopkg.in/macaroon-bakery.v2 v2.1.0 gopkg.in/macaroon.v2 v2.1.0 ) @@ -58,7 +61,7 @@ require ( github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/coreos/bbolt v1.3.3 // indirect github.com/coreos/go-semver v0.3.0 // indirect - github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f // indirect + github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -73,7 +76,6 @@ require ( github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect - github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.0.1 // indirect github.com/gorilla/websocket v1.4.2 // indirect @@ -108,7 +110,6 @@ require ( github.com/lightningnetwork/lnd/healthcheck v1.2.2 // indirect github.com/lightningnetwork/lnd/queue v1.1.0 // indirect github.com/lightningnetwork/lnd/ticker v1.1.0 // indirect - github.com/lightningnetwork/lnd/tor v1.0.1 // indirect github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect @@ -173,4 +174,6 @@ require ( sigs.k8s.io/yaml v1.2.0 // indirect ) +replace github.com/lightninglabs/lightning-terminal/autopilotserverrpc => ./autopilotserverrpc + go 1.18 diff --git a/go.sum b/go.sum index 718442188..297ff5d8f 100644 --- a/go.sum +++ b/go.sum @@ -81,6 +81,7 @@ github.com/btcsuite/btcd v0.22.0-beta.0.20220413172512-bf64c8bdbbbf/go.mod h1:0Q github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd v0.23.1/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd v0.23.2/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd v0.23.3/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd v0.23.4 h1:IzV6qqkfwbItOS/sg/aDfPDsjPP8twrCOE2R93hxMlQ= github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= @@ -168,8 +169,9 @@ github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= @@ -600,8 +602,9 @@ github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXj github.com/lightningnetwork/lnd/ticker v1.1.0 h1:ShoBiRP3pIxZHaETndfQ5kEe+S4NdAY1hiX7YbZ4QE4= github.com/lightningnetwork/lnd/ticker v1.1.0/go.mod h1:ubqbSVCn6RlE0LazXuBr7/Zi6QT0uQo++OgIRBxQUrk= github.com/lightningnetwork/lnd/tlv v1.0.2/go.mod h1:fICAfsqk1IOsC1J7G9IdsWX1EqWRMqEDCNxZJSKr9C4= -github.com/lightningnetwork/lnd/tlv v1.0.3 h1:0xBZcPuXagP6f7TY/RnLNR4igE21ov6qUdTr5NyvhhI= github.com/lightningnetwork/lnd/tlv v1.0.3/go.mod h1:dzR/aZetBri+ZY/fHbwV06fNn/3UID6htQzbHfREFdo= +github.com/lightningnetwork/lnd/tlv v1.1.0 h1:gsyte75HVuA/X59O+BhaISHM6OobZ0YesPbdu+xG1h0= +github.com/lightningnetwork/lnd/tlv v1.1.0/go.mod h1:0+JKp4un47MG1lnj6jKa8woNeB1X7w3yF4MZB1NHiiE= github.com/lightningnetwork/lnd/tor v1.0.0/go.mod h1:RDtaAdwfAm+ONuPYwUhNIH1RAvKPv+75lHPOegUcz64= github.com/lightningnetwork/lnd/tor v1.0.1 h1:A11FrpU0Y//g+fA827W4VnjOeoIvExONdchlLX8wYkA= github.com/lightningnetwork/lnd/tor v1.0.1/go.mod h1:RDtaAdwfAm+ONuPYwUhNIH1RAvKPv+75lHPOegUcz64= @@ -1237,8 +1240,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/itest/litd_accounts_test.go b/itest/litd_accounts_test.go index e0455c5d2..365f6a7a0 100644 --- a/itest/litd_accounts_test.go +++ b/itest/litd_accounts_test.go @@ -190,7 +190,7 @@ func testAccountRestrictionsLNC(ctxm context.Context, t *harnessTest, ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) defer cancel() - rawLNCConn, err := connectMailbox(ctxt, connectPhrase) + rawLNCConn, err := connectMailboxWithPairingPhrase(ctxt, connectPhrase) require.NoError(t.t, err) lightningClient := lnrpc.NewLightningClient(rawLNCConn) diff --git a/itest/litd_firewall_test.go b/itest/litd_firewall_test.go new file mode 100644 index 000000000..0d07e2456 --- /dev/null +++ b/itest/litd_firewall_test.go @@ -0,0 +1,1354 @@ +package itest + +import ( + "context" + "encoding/hex" + "fmt" + "io/ioutil" + "strconv" + "strings" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightninglabs/lightning-node-connect/mailbox" + "github.com/lightninglabs/lightning-terminal/autopilotserver/mock" + "github.com/lightninglabs/lightning-terminal/firewall" + "github.com/lightninglabs/lightning-terminal/litrpc" + "github.com/lightninglabs/lightning-terminal/rules" + "github.com/lightninglabs/lightning-terminal/session" + "github.com/lightningnetwork/lnd" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +const ( + // HeaderMacaroon is the HTTP header field name that is used to send + // the macaroon. + HeaderMacaroon = "Macaroon" +) + +var ( + rateLimit = &litrpc.RuleValue_RateLimit{ + RateLimit: &litrpc.RateLimit{ + ReadLimit: &litrpc.Rate{ + Iterations: 2, + NumHours: 1, + }, + WriteLimit: &litrpc.Rate{ + Iterations: 1, + NumHours: 1, + }, + }, + } + + policyBounds = &litrpc.RuleValue_ChanPolicyBounds{ + ChanPolicyBounds: &litrpc.ChannelPolicyBounds{ + MinBaseMsat: 100, + MaxBaseMsat: 1000, + MinRatePpm: 5000000, + MaxRatePpm: 10000000, + MinCltvDelta: 18, + MaxCltvDelta: 40, + MinHtlcMsat: 1000, + MaxHtlcMsat: 10000000, + }, + } + + historyLimit1 = &litrpc.RuleValue_HistoryLimit{ + HistoryLimit: &litrpc.HistoryLimit{ + Duration: uint64(time.Hour.Seconds() * 24 * 2), + }, + } + + historyLimit2 = &litrpc.RuleValue_HistoryLimit{ + HistoryLimit: &litrpc.HistoryLimit{ + StartTime: uint64( + time.Now().Add(-time.Hour * 24 * 3).Unix(), + ), + }, + } + + sendToSelf = &litrpc.RuleValue_SendToSelf{ + SendToSelf: &litrpc.SendToSelf{}, + } + + offChainBudget = &litrpc.RuleValue_OffChainBudget{ + OffChainBudget: &litrpc.OffChainBudget{ + MaxAmtMsat: 1200000000, + MaxFeesMsat: 200000, + }, + } + + chanPolicyBoundsRule = &mock.RuleRanges{ + Default: &rules.ChanPolicyBounds{ + MinBaseMsat: 0, + MaxBaseMsat: 10, + MinRatePPM: 1, + MaxRatePPM: 10, + MinCLTVDelta: 18, + MaxCLTVDelta: 18, + MinHtlcMsat: 1000, + MaxHtlcMsat: 10000, + }, + MinVal: &rules.ChanPolicyBounds{ + MinBaseMsat: 0, + MaxBaseMsat: 0, + MinRatePPM: 0, + MaxRatePPM: 0, + MinCLTVDelta: 18, + MaxCLTVDelta: 20, + MinHtlcMsat: 100, + MaxHtlcMsat: 1000, + }, + MaxVal: &rules.ChanPolicyBounds{ + MinBaseMsat: 1000, + MaxBaseMsat: 1000, + MinRatePPM: 5000000, + MaxRatePPM: 100000000, + MinCLTVDelta: 40, + MaxCLTVDelta: 60, + MinHtlcMsat: 100000, + MaxHtlcMsat: 1000000000000, + }, + } + + historyLimitRule = &mock.RuleRanges{ + Default: &rules.HistoryLimit{ + StartDate: time.Unix(0, 0), + }, + MinVal: &rules.HistoryLimit{ + Duration: time.Hour * 24, + }, + MaxVal: &rules.HistoryLimit{}, + } +) + +// testFWRateLimitAndPrivacyMapper tests that an Autopilot session is forced to +// adhere to the rate limits applied to the features of a session. Along the +// way, the privacy mapper is also tested. +func testFWRateLimitAndPrivacyMapper(net *NetworkHarness, t *harnessTest) { + ctx := context.Background() + + // Some very basic functionality tests to make sure lnd is working fine + // in integrated mode. + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, net.Alice) + + // We expect a non-empty alias (truncated node ID) to be returned. + resp, err := net.Alice.GetInfo(ctx, &lnrpc.GetInfoRequest{}) + require.NoError(t.t, err) + require.NotEmpty(t.t, resp.Alias) + require.Contains(t.t, resp.Alias, "0") + + // Open a channel between Alice and Bob so that we have something to + // query later. + channelOp := openChannelAndAssert( + t, net, net.Alice, net.Bob, lntest.OpenChannelParams{ + Amt: 100000, + }, + ) + defer closeChannelAndAssert(t, net, net.Alice, channelOp, true) + + // We extract the txid of the channel so that we can use it later to + // check that the autopilot's actions successfully completed and to + // check that the txid that the autopilot server sees is not the same + // as this one. + realTxidBytes, err := getChanPointFundingTxid(channelOp) + require.NoError(t.t, err) + realTxid, err := chainhash.NewHash(realTxidBytes) + require.NoError(t.t, err) + + // We create a connection to the Alice node's RPC server. + cfg := net.Alice.Cfg + rawConn, err := connectRPC(ctx, cfg.LitAddr(), cfg.TLSCertPath) + require.NoError(t.t, err) + + macBytes, err := ioutil.ReadFile(cfg.LitMacPath) + require.NoError(t.t, err) + ctxm := macaroonContext(ctx, macBytes) + + // Test that the connection to Alice's rpc server is working and that + // the autopilot server is returning a non-empty feature list. + litClient := litrpc.NewAutopilotClient(rawConn) + featResp, err := litClient.ListAutopilotFeatures( + ctxm, &litrpc.ListAutopilotFeaturesRequest{}, + ) + require.NoError(t.t, err) + require.NotEmpty(t.t, featResp) + + // Add a new Autopilot session that subscribes to both a "HealthCheck", + // and an "AutoFees" feature. Apply rate limits to the two features. + // This call is expected to also result in Litd registering this session + // with the mock autopilot server. + sessResp, err := litClient.AddAutopilotSession( + ctxm, &litrpc.AddAutopilotSessionRequest{ + Label: "integration-test", + ExpiryTimestampSeconds: uint64( + time.Now().Add(5 * time.Minute).Unix(), + ), + MailboxServerAddr: mailboxServerAddr, + Features: map[string]*litrpc.FeatureConfig{ + "HealthCheck": { + Rules: &litrpc.RulesMap{ + Rules: map[string]*litrpc.RuleValue{ + rules.RateLimitName: { + Value: rateLimit, + }, + }, + }, + }, + "AutoFees": { + Rules: &litrpc.RulesMap{ + Rules: map[string]*litrpc.RuleValue{ + rules.RateLimitName: { + Value: rateLimit, + }, + }, + }, + }, + }, + }, + ) + require.NoError(t.t, err) + + // From the response, we can extract Lit's local public key. + litdPub, err := btcec.ParsePubKey(sessResp.Session.LocalPublicKey) + require.NoError(t.t, err) + + // We then query the autopilot server to extract the private key that + // it will be using for this session. + pilotPriv, err := net.autopilotServer.GetPrivKey(litdPub) + require.NoError(t.t, err) + + // Now we can connect to the mailbox from the PoV of the autopilot + // server. + pilotConn, metaDataInjector, err := connectMailboxWithRemoteKey( + ctx, pilotPriv, litdPub, + ) + require.NoError(t.t, err) + defer pilotConn.Close() + lndConn := lnrpc.NewLightningClient(pilotConn) + + // The autopilot server is expected to add a MetaInfo caveat to any + // request that it makes. So we add that now and specify that it is + // initially making requests on behalf of the HealthCheck feature. + metaInfo := &firewall.InterceptMetaInfo{ + ActorName: "Autopilot Server", + Feature: "HealthCheck", + } + caveat, err := metaInfo.ToCaveat() + require.NoError(t.t, err) + caveatCreds := metaDataInjector.addCaveat(caveat) + + // Make first read request. Also check that the public key that the + // autopilot server receives from this request is not the same as + // Alice's actual public key. This is due to the actions of the privacy + // mapper. + getInfoReq, err := lndConn.GetInfo( + ctx, &lnrpc.GetInfoRequest{}, caveatCreds, + ) + require.NoError(t.t, err) + require.NotEqual( + t.t, hex.EncodeToString(net.Alice.PubKey[:]), + getInfoReq.IdentityPubkey, + ) + + // Make second read request. Also check that the privacy mapper is + // consistent with returning the same pseudo pub key. + getInfoReq2, err := lndConn.GetInfo( + ctx, &lnrpc.GetInfoRequest{}, caveatCreds, + ) + require.NoError(t.t, err) + require.Equal( + t.t, getInfoReq.IdentityPubkey, getInfoReq2.IdentityPubkey, + ) + + // The third read request should fail due to exceeding the read limit. + _, err = lndConn.GetInfo(ctx, &lnrpc.GetInfoRequest{}, caveatCreds) + assertStatusErr(t.t, err, codes.ResourceExhausted) + + // The autopilot should still be able to make calls for the AutoFees + // feature though. + metaInfo = &firewall.InterceptMetaInfo{ + ActorName: "Autopilot Server", + Feature: "AutoFees", + } + caveat, err = metaInfo.ToCaveat() + require.NoError(t.t, err) + caveatCreds = metaDataInjector.addCaveat(caveat) + + // GetInfo should not be allowed for the AutoFees feature. + _, err = lndConn.GetInfo(ctx, &lnrpc.GetInfoRequest{}, caveatCreds) + require.Error(t.t, err) + + // Make a valid read request for the feature. + channelsResp, err := lndConn.ListChannels( + ctx, &lnrpc.ListChannelsRequest{}, caveatCreds, + ) + require.NoError(t.t, err) + require.Len(t.t, channelsResp.Channels, 1) + + txid, index, err := decodeChannelPoint( + channelsResp.Channels[0].ChannelPoint, + ) + require.NoError(t.t, err) + + // Make sure that the txid returned by the call for Alice's channel is + // not the same as the real txid of the channel. + require.NotEqual(t.t, txid, realTxid.String()) + + chanPoint := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: txid, + }, + OutputIndex: index, + } + + // As the autopilot server, update the fees of the channel. + _, err = lndConn.UpdateChannelPolicy( + ctx, &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 9, + FeeRate: 8, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint, + }, + TimeLockDelta: 20, + MaxHtlcMsat: 100000, + }, caveatCreds, + ) + require.NoError(t.t, err) + + // Using the real txid, from the PoV of Alice, lets query this channel + // and make sure that the update successfully happened. + feeResp, err := net.Alice.FeeReport(ctx, &lnrpc.FeeReportRequest{}) + require.NoError(t.t, err) + require.Len(t.t, feeResp.ChannelFees, 1) + + txid2, _, err := decodeChannelPoint(feeResp.ChannelFees[0].ChannelPoint) + require.NoError(t.t, err) + require.Equal(t.t, realTxid.String(), txid2) + require.Equal(t.t, float64(8), feeResp.ChannelFees[0].FeeRate) + + // Now we will check the same thing but from the PoV of the autopilot + // server using the fake txid. + feeResp, err = lndConn.FeeReport( + ctx, &lnrpc.FeeReportRequest{}, caveatCreds, + ) + require.NoError(t.t, err) + require.Len(t.t, feeResp.ChannelFees, 1) + + txid3, _, err := decodeChannelPoint(feeResp.ChannelFees[0].ChannelPoint) + require.NoError(t.t, err) + require.Equal(t.t, txid, txid3) + require.Equal(t.t, float64(8), feeResp.ChannelFees[0].FeeRate) + + // Check that any more read or write requests from the autopilot server + // are disallowed due to the read and write rate limits being reached. + _, err = lndConn.FeeReport(ctx, &lnrpc.FeeReportRequest{}, caveatCreds) + assertStatusErr(t.t, err, codes.ResourceExhausted) + + _, err = lndConn.UpdateChannelPolicy( + ctx, &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 10, + FeeRate: 4, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint, + }, + TimeLockDelta: 20, + MaxHtlcMsat: 100000, + }, caveatCreds, + ) + assertStatusErr(t.t, err, codes.ResourceExhausted) +} + +// assertStatusErr asserts that the given error contains the given status code. +func assertStatusErr(t *testing.T, err error, code codes.Code) { + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), code.String())) +} + +// testFirewallRules tests that the various firewall rules are enforced +// correctly. +func testFirewallRules(net *NetworkHarness, t *harnessTest) { + ctx := context.Background() + + // Some very basic functionality tests to make sure lnd is working fine + // in integrated mode. + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, net.Alice) + + // We expect a non-empty alias (truncated node ID) to be returned. + resp, err := net.Alice.GetInfo(ctx, &lnrpc.GetInfoRequest{}) + require.NoError(t.t, err) + require.NotEmpty(t.t, resp.Alias) + require.Contains(t.t, resp.Alias, "0") + + // Open a channel between Alice and Bob so that we have something to + // query later. + channelOp := openChannelAndAssert( + t, net, net.Alice, net.Bob, lntest.OpenChannelParams{ + Amt: 100000, + }, + ) + defer closeChannelAndAssert(t, net, net.Alice, channelOp, true) + + t.t.Run("history limit rule", func(_ *testing.T) { + testHistoryLimitRule(net, t) + }) + + t.t.Run("channel policy bounds rule", func(_ *testing.T) { + testChanPolicyBoundsRule(net, t) + }) + + t.t.Run("peer and channel restrict rules", func(_ *testing.T) { + testPeerAndChannelRestrictRules(net, t) + }) +} + +// testHistoryLimitRule tests that the autopilot server is forced to adhere to +// the history-limit rule. +func testHistoryLimitRule(net *NetworkHarness, t *harnessTest) { + ctx := context.Background() + + // We create a connection to the Alice node's RPC server. + cfg := net.Alice.Cfg + rawConn, err := connectRPC(ctx, cfg.LitAddr(), cfg.TLSCertPath) + require.NoError(t.t, err) + + macBytes, err := ioutil.ReadFile(cfg.LitMacPath) + require.NoError(t.t, err) + ctxm := macaroonContext(ctx, macBytes) + + // Now we will override the autopilots features set so that we can + // just focus on the fee bounds rule for now. + net.autopilotServer.SetFeatures(map[string]*mock.Feature{ + "AutoFees": { + Description: "manages your channel fees", + Rules: map[string]*mock.RuleRanges{ + rules.HistoryLimitName: historyLimitRule, + }, + Permissions: map[string][]bakery.Op{ + "/lnrpc.Lightning/ForwardingHistory": {{ + Entity: "offchain", + Action: "read", + }}, + }, + }, + "AutoFees2": { + Description: "manages your channel fees another way!", + Rules: map[string]*mock.RuleRanges{ + rules.HistoryLimitName: historyLimitRule, + }, + Permissions: map[string][]bakery.Op{ + "/lnrpc.Lightning/ForwardingHistory": {{ + Entity: "offchain", + Action: "read", + }}, + }, + }, + }) + + // Test that the connection to Alice's rpc server is working and that + // the autopilot server is returning a non-empty feature list. + litClient := litrpc.NewAutopilotClient(rawConn) + featResp, err := litClient.ListAutopilotFeatures( + ctxm, &litrpc.ListAutopilotFeaturesRequest{}, + ) + require.NoError(t.t, err) + require.NotEmpty(t.t, featResp) + + // Add a new Autopilot session that subscribes to the "AutoFees" feature + // and apply a history limit that uses a Duration to it. Then we will + // also add an "AutoFees2" feature and apply a history limit that uses + // a Start-data to it. + sessResp, err := litClient.AddAutopilotSession( + ctxm, &litrpc.AddAutopilotSessionRequest{ + Label: "integration-test", + ExpiryTimestampSeconds: uint64( + time.Now().Add(5 * time.Minute).Unix(), + ), + MailboxServerAddr: mailboxServerAddr, + Features: map[string]*litrpc.FeatureConfig{ + "AutoFees": { + Rules: &litrpc.RulesMap{ + Rules: map[string]*litrpc.RuleValue{ + rules.HistoryLimitName: { + Value: historyLimit1, + }, + }, + }, + }, + "AutoFees2": { + Rules: &litrpc.RulesMap{ + Rules: map[string]*litrpc.RuleValue{ + rules.HistoryLimitName: { + Value: historyLimit2, + }, + }, + }, + }, + }, + }, + ) + require.NoError(t.t, err) + + // From the response, we can extract Lit's local public key. + litdPub, err := btcec.ParsePubKey(sessResp.Session.LocalPublicKey) + require.NoError(t.t, err) + + // We then query the autopilot server to extract the private key that + // it will be using for this session. + pilotPriv, err := net.autopilotServer.GetPrivKey(litdPub) + require.NoError(t.t, err) + + // Now we can connect to the mailbox from the PoV of the autopilot + // server. + pilotConn, metaDataInjector, err := connectMailboxWithRemoteKey( + ctx, pilotPriv, litdPub, + ) + require.NoError(t.t, err) + defer pilotConn.Close() + + lndConn := lnrpc.NewLightningClient(pilotConn) + + // First, we will test the "AutoFees" feature which uses a duration to + // specify a history limit. The duration specified is 48 hours and so + // the autopilot should not be able to query for more data more than 48 + // hours old. + + // The autopilot server is expected to add a MetaInfo caveat to any + // request that it makes. So we add that now and specify that it is + // initially making requests on behalf of the AutoFees feature. + metaInfo := &firewall.InterceptMetaInfo{ + ActorName: "Autopilot Server", + Feature: "AutoFees", + } + caveat, err := metaInfo.ToCaveat() + require.NoError(t.t, err) + caveatCreds := metaDataInjector.addCaveat(caveat) + + // If the autopilot queries for data starting from 72 hours ago, the + // call should fail. + _, err = lndConn.ForwardingHistory( + ctx, &lnrpc.ForwardingHistoryRequest{ + StartTime: uint64( + time.Now().Add(-time.Hour * 24 * 3).Unix(), + ), + }, caveatCreds, + ) + assertStatusErr(t.t, err, codes.ResourceExhausted) + + // If the autopilot queries for data starting from somewhere within the + // last 48 hours, the call should succeed. + _, err = lndConn.ForwardingHistory( + ctx, &lnrpc.ForwardingHistoryRequest{ + StartTime: uint64( + time.Now().Add(-time.Hour * 24).Unix(), + ), + }, caveatCreds, + ) + require.NoError(t.t, err) + + // Now, we will test the "AutoFees2" feature which uses a start date to + // specify a history limit. The start date specified is now minus 72 + // hours and so the autopilot should not be able to query for more data + // more than 72 hours old. + metaInfo.Feature = "AutoFees2" + caveat, err = metaInfo.ToCaveat() + require.NoError(t.t, err) + caveatCreds = metaDataInjector.addCaveat(caveat) + + // If the autopilot queries for data starting from 96 hours ago, the + // call should fail. + _, err = lndConn.ForwardingHistory( + ctx, &lnrpc.ForwardingHistoryRequest{ + StartTime: uint64( + time.Now().Add(-time.Hour * 24 * 4).Unix(), + ), + }, caveatCreds, + ) + assertStatusErr(t.t, err, codes.ResourceExhausted) + + // If the autopilot queries for data starting from somewhere within the + // last 72 hours, the call should succeed. + _, err = lndConn.ForwardingHistory( + ctx, &lnrpc.ForwardingHistoryRequest{ + StartTime: uint64( + time.Now().Add(-time.Hour * 24 * 2).Unix(), + ), + }, caveatCreds, + ) + require.NoError(t.t, err) +} + +// testChanPolicyBoundsRule tests that the autopilot server is forced to adhere +// to the fee-bounds rule. +func testChanPolicyBoundsRule(net *NetworkHarness, t *harnessTest) { + ctx := context.Background() + + // We create a connection to the Alice node's RPC server. + cfg := net.Alice.Cfg + rawConn, err := connectRPC(ctx, cfg.LitAddr(), cfg.TLSCertPath) + require.NoError(t.t, err) + + macBytes, err := ioutil.ReadFile(cfg.LitMacPath) + require.NoError(t.t, err) + ctxm := macaroonContext(ctx, macBytes) + + // Now we will override the autopilots features set so that we can + // just focus on the fee bounds rule for now. + net.autopilotServer.SetFeatures(map[string]*mock.Feature{ + "AutoFees": { + Description: "manages your channel fees", + Rules: map[string]*mock.RuleRanges{ + rules.ChanPolicyBoundsName: chanPolicyBoundsRule, + }, + Permissions: map[string][]bakery.Op{ + "/lnrpc.Lightning/UpdateChannelPolicy": {{ + Entity: "offchain", + Action: "write", + }}, + "/lnrpc.Lightning/FeeReport": {{ + Entity: "offchain", + Action: "read", + }}, + }, + }, + }) + + // Test that the connection to Alice's rpc server is working and that + // the autopilot server is returning a non-empty feature list. + litClient := litrpc.NewAutopilotClient(rawConn) + featResp, err := litClient.ListAutopilotFeatures( + ctxm, &litrpc.ListAutopilotFeaturesRequest{}, + ) + require.NoError(t.t, err) + require.NotEmpty(t.t, featResp) + + // Add a new Autopilot session that subscribes to the "AutoFees" feature + // and apply a rate limit to it. + sessResp, err := litClient.AddAutopilotSession( + ctxm, &litrpc.AddAutopilotSessionRequest{ + Label: "integration-test", + ExpiryTimestampSeconds: uint64( + time.Now().Add(5 * time.Minute).Unix(), + ), + MailboxServerAddr: mailboxServerAddr, + Features: map[string]*litrpc.FeatureConfig{ + "AutoFees": { + Rules: &litrpc.RulesMap{ + Rules: map[string]*litrpc.RuleValue{ + rules.ChanPolicyBoundsName: { + Value: policyBounds, + }, + }, + }, + }, + }, + }, + ) + require.NoError(t.t, err) + + // From the response, we can extract Lit's local public key. + litdPub, err := btcec.ParsePubKey(sessResp.Session.LocalPublicKey) + require.NoError(t.t, err) + + // We then query the autopilot server to extract the private key that + // it will be using for this session. + pilotPriv, err := net.autopilotServer.GetPrivKey(litdPub) + require.NoError(t.t, err) + + // Now we can connect to the mailbox from the PoV of the autopilot + // server. + pilotConn, metaDataInjector, err := connectMailboxWithRemoteKey( + ctx, pilotPriv, litdPub, + ) + require.NoError(t.t, err) + defer pilotConn.Close() + lndConn := lnrpc.NewLightningClient(pilotConn) + + // The autopilot server is expected to add a MetaInfo caveat to any + // request that it makes. So we add that now and specify that it is + // initially making requests on behalf of the HealthCheck feature. + metaInfo := &firewall.InterceptMetaInfo{ + ActorName: "Autopilot Server", + Feature: "AutoFees", + } + caveat, err := metaInfo.ToCaveat() + require.NoError(t.t, err) + caveatCreds := metaDataInjector.addCaveat(caveat) + + // First, get the current fee rates of the litd node. + var chanFees *lnrpc.ChannelFeeReport + assertFees := func(expectedFeeRate float64, expectedBase int64) { + feeResp, err := lndConn.FeeReport( + ctx, &lnrpc.FeeReportRequest{}, caveatCreds, + ) + require.NoError(t.t, err) + require.Len(t.t, feeResp.ChannelFees, 1) + require.Equal( + t.t, expectedFeeRate, feeResp.ChannelFees[0].FeeRate, + ) + require.Equal( + t.t, expectedBase, feeResp.ChannelFees[0].BaseFeeMsat, + ) + chanFees = feeResp.ChannelFees[0] + } + + assertFees(1e-06, 1000) + + txid, index, err := decodeChannelPoint(chanFees.ChannelPoint) + require.NoError(t.t, err) + + chanPoint := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: txid, + }, + OutputIndex: index, + } + + // Now try to update the fees by violating the minimum allowed base + // fee rate. This should fail. + _, err = lndConn.UpdateChannelPolicy( + ctx, &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 10, + FeeRate: 4, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint, + }, + TimeLockDelta: 20, + MaxHtlcMsat: 100000, + }, caveatCreds, + ) + assertStatusErr(t.t, err, codes.ResourceExhausted) + assertFees(1e-06, 1000) + + // Base fee too high. + _, err = lndConn.UpdateChannelPolicy( + ctx, &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 60000, + FeeRate: 4, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint, + }, + TimeLockDelta: 20, + MaxHtlcMsat: 100000, + }, caveatCreds, + ) + assertStatusErr(t.t, err, codes.ResourceExhausted) + assertFees(1e-06, 1000) + + // Fee rate too low. + _, err = lndConn.UpdateChannelPolicy( + ctx, &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 500, + FeeRate: 0, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint, + }, + TimeLockDelta: 10, + MaxHtlcMsat: 100000, + }, caveatCreds, + ) + assertStatusErr(t.t, err, codes.ResourceExhausted) + assertFees(1e-06, 1000) + + // Fee rate too high. + _, err = lndConn.UpdateChannelPolicy( + ctx, &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 500, + FeeRate: 100, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint, + }, + TimeLockDelta: 20, + MaxHtlcMsat: 100000, + }, caveatCreds, + ) + assertStatusErr(t.t, err, codes.ResourceExhausted) + assertFees(1e-06, 1000) + + // Valid fee change! + _, err = lndConn.UpdateChannelPolicy( + ctx, &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 500, + FeeRate: 7, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint, + }, + TimeLockDelta: 20, + MaxHtlcMsat: 100000, + }, caveatCreds, + ) + require.NoError(t.t, err) + assertFees(7, 500) +} + +func testPeerAndChannelRestrictRules(net *NetworkHarness, t *harnessTest) { + ctx := context.Background() + + charlie, err := net.NewNode(t.t, "Charlie", nil, false, true) + require.NoError(t.t, err) + defer shutdownAndAssert(net, t, charlie) + + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, charlie) + net.EnsureConnected(t.t, net.Alice, charlie) + + // Open another channel between Alice and Bob. + channelAB2Op := openChannelAndAssert( + t, net, net.Alice, net.Bob, lntest.OpenChannelParams{ + Amt: 100000, + }, + ) + defer closeChannelAndAssert(t, net, net.Alice, channelAB2Op, true) + + // Open two channels between Alice and Charlie. + channelAC1Op := openChannelAndAssert( + t, net, net.Alice, charlie, lntest.OpenChannelParams{ + Amt: 100000, + }, + ) + defer closeChannelAndAssert(t, net, net.Alice, channelAC1Op, true) + + channelAC2Op := openChannelAndAssert( + t, net, net.Alice, charlie, lntest.OpenChannelParams{ + Amt: 100000, + }, + ) + defer closeChannelAndAssert(t, net, net.Alice, channelAC2Op, true) + + // List Alice's channels so that we can extract the channel points + // for the four channels. + chans, err := net.Alice.ListChannels(ctx, &lnrpc.ListChannelsRequest{}) + require.NoError(t.t, err) + require.Len(t.t, chans.Channels, 4) + + var chanABPoints, chanACPoints []string + var chanToRestrict uint64 + for _, c := range chans.Channels { + if c.RemotePubkey == hex.EncodeToString(net.Bob.PubKey[:]) { + chanABPoints = append(chanABPoints, c.ChannelPoint) + continue + } + chanACPoints = append(chanACPoints, c.ChannelPoint) + chanToRestrict = c.ChanId + } + + // We create a connection to the Alice node's RPC server. + cfg := net.Alice.Cfg + rawConn, err := connectRPC(ctx, cfg.LitAddr(), cfg.TLSCertPath) + require.NoError(t.t, err) + + macBytes, err := ioutil.ReadFile(cfg.LitMacPath) + require.NoError(t.t, err) + ctxm := macaroonContext(ctx, macBytes) + + // Now we will override the autopilots features set so that we can + // just focus on the peer and channel restriction rules for now. + net.autopilotServer.SetFeatures(map[string]*mock.Feature{ + "AutoFees": { + Description: "manages your channel fees", + Rules: map[string]*mock.RuleRanges{ + rules.PeersRestrictName: { + Default: &rules.PeerRestrict{}, + MinVal: &rules.PeerRestrict{}, + MaxVal: &rules.PeerRestrict{}, + }, + rules.ChannelRestrictName: { + Default: &rules.ChannelRestrict{}, + MinVal: &rules.ChannelRestrict{}, + MaxVal: &rules.ChannelRestrict{}, + }, + }, + Permissions: map[string][]bakery.Op{ + "/lnrpc.Lightning/UpdateChannelPolicy": {{ + Entity: "offchain", + Action: "write", + }}, + "/lnrpc.Lightning/FeeReport": {{ + Entity: "offchain", + Action: "read", + }}, + }, + }, + }) + + // Test that the connection to Alice's rpc server is working and that + // the autopilot server is returning a non-empty feature list. + litCAutopilotClient := litrpc.NewAutopilotClient(rawConn) + featResp, err := litCAutopilotClient.ListAutopilotFeatures( + ctxm, &litrpc.ListAutopilotFeaturesRequest{}, + ) + require.NoError(t.t, err) + require.NotEmpty(t.t, featResp) + + // Construct a peer-restriction list using Bob's pub key. + bobKey := hex.EncodeToString(net.Bob.PubKey[:]) + peerRestrict := &litrpc.RuleValue_PeerRestrict{ + PeerRestrict: &litrpc.PeerRestrict{ + PeerIds: []string{bobKey}, + }, + } + + // Next, construct a channel-restriction list using one of the channels + channelRestrict := &litrpc.RuleValue_ChannelRestrict{ + ChannelRestrict: &litrpc.ChannelRestrict{ + ChannelIds: []uint64{chanToRestrict}, + }, + } + + // Add a new Autopilot session that subscribes to the "AutoFees" feature + // and apply a peer restriction list to it. + sessResp, err := litCAutopilotClient.AddAutopilotSession( + ctxm, &litrpc.AddAutopilotSessionRequest{ + Label: "integration-test", + ExpiryTimestampSeconds: uint64( + time.Now().Add(5 * time.Minute).Unix(), + ), + MailboxServerAddr: mailboxServerAddr, + Features: map[string]*litrpc.FeatureConfig{ + "AutoFees": { + Rules: &litrpc.RulesMap{ + Rules: map[string]*litrpc.RuleValue{ + rules.PeersRestrictName: { + Value: peerRestrict, + }, + rules.ChannelRestrictName: { + Value: channelRestrict, + }, + }, + }, + }, + }, + }, + ) + require.NoError(t.t, err) + + // From the response, we can extract Lit's local public key. + litdPub, err := btcec.ParsePubKey(sessResp.Session.LocalPublicKey) + require.NoError(t.t, err) + + // We then query the autopilot server to extract the private key that + // it will be using for this session. + pilotPriv, err := net.autopilotServer.GetPrivKey(litdPub) + require.NoError(t.t, err) + + // Now we can connect to the mailbox from the PoV of the autopilot + // server. + pilotConn, metaDataInjector, err := connectMailboxWithRemoteKey( + ctx, pilotPriv, litdPub, + ) + require.NoError(t.t, err) + defer pilotConn.Close() + lndConn := lnrpc.NewLightningClient(pilotConn) + + // The autopilot server is expected to add a MetaInfo caveat to any + // request that it makes. So we add that now and specify that it is + // initially making requests on behalf of the HealthCheck feature. + metaInfo := &firewall.InterceptMetaInfo{ + ActorName: "Autopilot Server", + Feature: "AutoFees", + } + caveat, err := metaInfo.ToCaveat() + require.NoError(t.t, err) + caveatCreds := metaDataInjector.addCaveat(caveat) + + // From the PoV of the Autopilot server, we do a quick FeeReport call. + // This will force the Privacy Mapper on Alice's node to create the + // real-pseudo pairs for all her channel id's and points. We do this + // so that we can know which channels we are referring to when we make + // calls from the autopilot later on in this test. + feeReport, err := lndConn.FeeReport( + ctx, &lnrpc.FeeReportRequest{}, caveatCreds, + ) + require.NoError(t.t, err) + require.Len(t.t, feeReport.ChannelFees, 4) + + // Query Alice's privacy mapper to see which pseudo channel ID the + // autopilot should use when it tries to update one of the channels + // that Alice has with Bob. + litClient := litrpc.NewFirewallClient(rawConn) + privMapResp, err := litClient.PrivacyMapConversion( + ctxm, &litrpc.PrivacyMapConversionRequest{ + SessionId: sessResp.Session.Id, + RealToPseudo: true, + Input: chanABPoints[0], + }, + ) + require.NoError(t.t, err) + + pseudoChanPoint := privMapResp.Output + pseudoTxid, pseudoIndex, err := decodeChannelPoint(pseudoChanPoint) + require.NoError(t.t, err) + + // Make sure that this channel point did in fact appear in the list + // provided to the autopilot. + var found bool + for _, c := range feeReport.ChannelFees { + if pseudoChanPoint == c.ChannelPoint { + found = true + } + } + require.True(t.t, found) + + // Now, from the autopilot's PoV, try to update the fees on this + // channel. It should fail due to the fact that the channel peer, Bob, + // is in the peer-restriction rule that Alice applied to the session. + chanPoint := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: pseudoTxid, + }, + OutputIndex: pseudoIndex, + } + _, err = lndConn.UpdateChannelPolicy( + ctx, &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 9, + FeeRate: 8, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint, + }, + TimeLockDelta: 20, + MaxHtlcMsat: 100000, + }, caveatCreds, + ) + assertStatusErr(t.t, err, codes.ResourceExhausted) + require.ErrorContains(t.t, err, "illegal action on peer in peer "+ + "restriction list") + + // We now do the same thing for the other channel that Alice has with + // Bob. This should also fail due to Bob being on the peer-restrict + // list. + privMapResp, err = litClient.PrivacyMapConversion( + ctxm, &litrpc.PrivacyMapConversionRequest{ + SessionId: sessResp.Session.Id, + RealToPseudo: true, + Input: chanABPoints[1], + }, + ) + require.NoError(t.t, err) + + pseudoChanPoint = privMapResp.Output + pseudoTxid, pseudoIndex, err = decodeChannelPoint(pseudoChanPoint) + require.NoError(t.t, err) + + // Now, from the autopilot's PoV, try to update the fees on this + // channel. It should fail due to the fact that the channel peer, Bob, + // is in the peer-restriction rule that Alice applied to the session. + chanPoint = &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: pseudoTxid, + }, + OutputIndex: pseudoIndex, + } + _, err = lndConn.UpdateChannelPolicy( + ctx, &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 9, + FeeRate: 8, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint, + }, + TimeLockDelta: 20, + MaxHtlcMsat: 100000, + }, caveatCreds, + ) + assertStatusErr(t.t, err, codes.ResourceExhausted) + require.ErrorContains(t.t, err, "illegal action on peer in peer "+ + "restriction list") + + // Again we query Alice's privacy mapper to see which pseudo channel ID + // the autopilot should use for the channel she has with Charlie. + // We first do this for the channel that is in the channel restrict + // list. + privMapResp, err = litClient.PrivacyMapConversion( + ctxm, &litrpc.PrivacyMapConversionRequest{ + SessionId: sessResp.Session.Id, + RealToPseudo: true, + Input: chanACPoints[1], + }, + ) + require.NoError(t.t, err) + + pseudoChanPoint = privMapResp.Output + pseudoTxid, pseudoIndex, err = decodeChannelPoint(pseudoChanPoint) + require.NoError(t.t, err) + + // The call to update the channel policy should fail due to this + // channel being in the channel-restrict list. + chanPoint = &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: pseudoTxid, + }, + OutputIndex: pseudoIndex, + } + _, err = lndConn.UpdateChannelPolicy( + ctx, &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 9, + FeeRate: 8, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint, + }, + TimeLockDelta: 20, + MaxHtlcMsat: 100000, + }, caveatCreds, + ) + assertStatusErr(t.t, err, codes.ResourceExhausted) + require.ErrorContains(t.t, err, "illegal action on channel in channel "+ + "restriction list") + + // Finally, we repeat the test on the other channel that Alice has + // with Charlie (the channel _not_ in the channel-restrict list). + privMapResp, err = litClient.PrivacyMapConversion( + ctxm, &litrpc.PrivacyMapConversionRequest{ + SessionId: sessResp.Session.Id, + RealToPseudo: true, + Input: chanACPoints[0], + }, + ) + require.NoError(t.t, err) + + pseudoChanPoint = privMapResp.Output + pseudoTxid, pseudoIndex, err = decodeChannelPoint(pseudoChanPoint) + require.NoError(t.t, err) + + // The call to update the channel policy should succeed. + chanPoint = &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: pseudoTxid, + }, + OutputIndex: pseudoIndex, + } + _, err = lndConn.UpdateChannelPolicy( + ctx, &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 9, + FeeRate: 8, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint, + }, + TimeLockDelta: 20, + MaxHtlcMsat: 100000, + }, caveatCreds, + ) + require.NoError(t.t, err) +} + +func testLargeHttpHeader(net *NetworkHarness, t *harnessTest) { + ctx := context.Background() + + // First we add all LND's permissions so that any call we make to LND to + // test that the connection is working will succeed. + perms := lnd.MainRPCServerPermissions() + + // Now we pad the above valid perms with a bunch of junk perms. This is + // done in order to make the macaroon that will be sent in the header + // very big. + for i := 0; i < 800; i++ { + uniqueString := fmt.Sprintf("unique long string %d", i) + perms[uniqueString] = []bakery.Op{ + { + Entity: uniqueString, + Action: "fake-action", + }, + } + } + + // We first override the autopilots features set. We create a feature + // with a very large permissions set. This is done so that we can test + // that a grpc/http header of a certain size does not break things. + net.autopilotServer.SetFeatures(map[string]*mock.Feature{ + "TestFeature": { + Rules: map[string]*mock.RuleRanges{}, + Permissions: perms, + }, + }) + + // We expect a non-empty alias to be returned. + aliceInfo, err := net.Alice.GetInfo(ctx, &lnrpc.GetInfoRequest{}) + require.NoError(t.t, err) + require.NotEmpty(t.t, aliceInfo.Alias) + + // We create a connection to the Alice node's RPC server. + cfg := net.Alice.Cfg + rawConn, err := connectRPC(ctx, cfg.LitAddr(), cfg.TLSCertPath) + require.NoError(t.t, err) + + macBytes, err := ioutil.ReadFile(cfg.LitMacPath) + require.NoError(t.t, err) + ctxm := macaroonContext(ctx, macBytes) + + // Test that the connection to Alice's rpc server is working and that + // the autopilot server is returning a non-empty feature list. + litClient := litrpc.NewAutopilotClient(rawConn) + featResp, err := litClient.ListAutopilotFeatures( + ctxm, &litrpc.ListAutopilotFeaturesRequest{}, + ) + require.NoError(t.t, err) + require.NotEmpty(t.t, featResp) + + // Add a new Autopilot session that subscribes to a "Test", feature. + // This call is expected to also result in Litd registering this session + // with the mock autopilot server. + sessResp, err := litClient.AddAutopilotSession( + ctxm, &litrpc.AddAutopilotSessionRequest{ + Label: "integration-test", + ExpiryTimestampSeconds: uint64( + time.Now().Add(5 * time.Minute).Unix(), + ), + MailboxServerAddr: mailboxServerAddr, + Features: map[string]*litrpc.FeatureConfig{ + "TestFeature": { + Rules: &litrpc.RulesMap{ + Rules: map[string]*litrpc.RuleValue{}, + }, + }, + }, + // Switch the privacy mapper off for simplicity’s sake. + NoPrivacyMapper: true, + }, + ) + require.NoError(t.t, err) + + // From the response, we can extract Lit's local public key. + litdPub, err := btcec.ParsePubKey(sessResp.Session.LocalPublicKey) + require.NoError(t.t, err) + + // We then query the autopilot server to extract the private key that + // it will be using for this session. + pilotPriv, err := net.autopilotServer.GetPrivKey(litdPub) + require.NoError(t.t, err) + + // Now we can connect to the mailbox from the PoV of the autopilot + // server. + pilotConn, metaDataInjector, err := connectMailboxWithRemoteKey( + ctx, pilotPriv, litdPub, + ) + require.NoError(t.t, err) + defer pilotConn.Close() + lndConn := lnrpc.NewLightningClient(pilotConn) + + // The autopilot server is expected to add a MetaInfo caveat to any + // request that it makes. So we add that now and specify that it is + // initially making requests on behalf of the HealthCheck feature. + metaInfo := &firewall.InterceptMetaInfo{ + ActorName: "Autopilot Server", + Feature: "TestFeature", + } + caveat, err := metaInfo.ToCaveat() + require.NoError(t.t, err) + caveatCreds := metaDataInjector.addCaveat(caveat) + + // Now we assert that the size of the macaroon that will go in the + // request header is larger than a certain threshold. + meta, err := caveatCreds.Creds.GetRequestMetadata(ctx, "") + require.NoError(t.t, err) + require.Greater(t.t, len([]byte(meta[HeaderMacaroon])), 40000) + + // Assert that requests from the autopilot work with this large header. + getInfoReq, err := lndConn.GetInfo( + ctx, &lnrpc.GetInfoRequest{}, caveatCreds, + ) + require.NoError(t.t, err) + require.Equal(t.t, aliceInfo.Alias, getInfoReq.Alias) +} + +// connectMailboxWithRemoteKey tries to establish a connection through LNC using +// the given local and remote keys and the test mailbox server. +func connectMailboxWithRemoteKey(ctx context.Context, + localKey *btcec.PrivateKey, remoteKey *btcec.PublicKey) ( + *grpc.ClientConn, *metadataInjector, error) { + + ecdh := &keychain.PrivKeyECDH{PrivKey: localKey} + connData := mailbox.NewConnData(ecdh, remoteKey, nil, nil, nil, nil) + + transportConn, err := mailbox.NewClient(ctx, connData) + if err != nil { + return nil, nil, err + } + + noiseConn := mailbox.NewNoiseGrpcConn(connData) + + dialOpts := []grpc.DialOption{ + grpc.WithContextDialer(transportConn.Dial), + grpc.WithTransportCredentials(noiseConn), + grpc.WithBlock(), + } + + conn, err := grpc.DialContext(ctx, mailboxServerAddr, dialOpts...) + return conn, &metadataInjector{originalCredentials: noiseConn}, err +} + +type metadataInjector struct { + originalCredentials credentials.PerRPCCredentials +} + +func (m *metadataInjector) addCaveat(caveat string) grpc.PerRPCCredsCallOption { + return grpc.PerRPCCredsCallOption{ + Creds: &caveatCredentials{ + originalCredentials: m.originalCredentials, + caveat: caveat, + }, + } +} + +type caveatCredentials struct { + originalCredentials credentials.PerRPCCredentials + caveat string +} + +func (c *caveatCredentials) GetRequestMetadata(ctx context.Context, + uri ...string) (map[string]string, error) { + + metadata, err := c.originalCredentials.GetRequestMetadata(ctx, uri...) + if err != nil { + return nil, err + } + + macHex, ok := metadata[HeaderMacaroon] + if !ok || len(macHex) == 0 { + return metadata, nil + } + + mac, err := session.ParseMacaroon(macHex) + if err != nil { + return nil, err + } + + if err := mac.AddFirstPartyCaveat([]byte(c.caveat)); err != nil { + return nil, err + } + + macBytes, err := mac.MarshalBinary() + if err != nil { + return nil, err + } + + metadata[HeaderMacaroon] = hex.EncodeToString(macBytes) + + return metadata, nil +} + +// RequireTransportSecurity indicates whether the credentials requires +// transport security. +func (c *caveatCredentials) RequireTransportSecurity() bool { + return true +} + +func decodeChannelPoint(cp string) (string, uint32, error) { + parts := strings.Split(cp, ":") + if len(parts) != 2 { + return "", 0, fmt.Errorf("bad channel point encoding") + } + + index, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return "", 0, err + } + + return parts[0], uint32(index), nil +} diff --git a/itest/litd_mode_integrated_test.go b/itest/litd_mode_integrated_test.go index 1c0e07f0a..cc7ac75cf 100644 --- a/itest/litd_mode_integrated_test.go +++ b/itest/litd_mode_integrated_test.go @@ -153,6 +153,14 @@ var ( litConn := litrpc.NewAccountsClient(c) return litConn.ListAccounts(ctx, &litrpc.ListAccountsRequest{}) } + litAutopilotRequestFn = func(ctx context.Context, + c grpc.ClientConnInterface) (proto.Message, error) { + + litConn := litrpc.NewAutopilotClient(c) + return litConn.ListAutopilotFeatures( + ctx, &litrpc.ListAutopilotFeaturesRequest{}, + ) + } litMacaroonFn = func(cfg *LitNodeConfig) string { return cfg.LitMacPath } @@ -232,6 +240,13 @@ var ( successPattern: "\"accounts\":[]", allowedThroughLNC: false, grpcWebURI: "/litrpc.Accounts/ListAccounts", + }, { + name: "litrpc-autopilot", + macaroonFn: litMacaroonFn, + requestFn: litAutopilotRequestFn, + successPattern: "\"features\":{", + allowedThroughLNC: true, + grpcWebURI: "/litrpc.Autopilot/ListAutopilotFeatures", }} // customURIs is a map of endpoint URIs that we want to allow via a @@ -501,7 +516,7 @@ func setUpLNCConn(ctx context.Context, t *testing.T, hostPort, tlsCertPath, sessResp.Session.PairingSecretMnemonic, " ", ) - rawLNCConn, err := connectMailbox(ctx, connectPhrase) + rawLNCConn, err := connectMailboxWithPairingPhrase(ctx, connectPhrase) require.NoError(t, err) return rawLNCConn @@ -861,9 +876,9 @@ func getServerCertificates(hostPort string) ([]*x509.Certificate, error) { return conn.ConnectionState().PeerCertificates, nil } -// connectMailbox tries to establish a connection through LNC using the given -// connect phrase and the test mailbox server. -func connectMailbox(ctx context.Context, +// connectMailboxWithPairingPhrase tries to establish a connection through LNC +// using the given connect phrase and the test mailbox server. +func connectMailboxWithPairingPhrase(ctx context.Context, connectPhrase []string) (*grpc.ClientConn, error) { var mnemonicWords [mailbox.NumPassphraseWords]string diff --git a/itest/litd_test_list_on_test.go b/itest/litd_test_list_on_test.go index 24c973cf8..282f5b576 100644 --- a/itest/litd_test_list_on_test.go +++ b/itest/litd_test_list_on_test.go @@ -12,4 +12,12 @@ var allTestCases = []*testCase{ name: "test mode remote", test: testModeRemote, }, + { + name: "test firewall rules", + test: testFirewallRules, + }, + { + name: "test large http header", + test: testLargeHttpHeader, + }, } diff --git a/itest/network_harness.go b/itest/network_harness.go index 6ff01ce6e..f529c23ba 100644 --- a/itest/network_harness.go +++ b/itest/network_harness.go @@ -18,6 +18,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lightning-terminal/autopilotserver/mock" "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntest" @@ -55,6 +56,8 @@ type NetworkHarness struct { nodesByPub map[string]*HarnessNode + autopilotServer *mock.Server + // Alice and Bob are the initial seeder nodes that are automatically // created to be the initial participants of the test network. Alice *HarnessNode @@ -130,6 +133,10 @@ func (n *NetworkHarness) SetUp(t *testing.T, err := n.server.start() require.NoError(t, err) + // Start a mock autopilot server. + n.autopilotServer = mock.NewServer() + require.NoError(t, n.autopilotServer.Start()) + // Start the initial seeder nodes within the test network, then connect // their respective RPC clients. eg := errgroup.Group{} @@ -260,6 +267,8 @@ func (n *NetworkHarness) TearDown() error { func (n *NetworkHarness) Stop() { close(n.lndErrorChan) close(n.quit) + + n.autopilotServer.Stop() } // NewNode initializes a new HarnessNode. @@ -271,6 +280,11 @@ func (n *NetworkHarness) NewNode(t *testing.T, name string, extraArgs []string, fmt.Sprintf("--loop.server.tlspath=%s", n.server.certFile), fmt.Sprintf("--pool.auctionserver=%s", n.server.serverHost), fmt.Sprintf("--pool.tlspathauctserver=%s", n.server.certFile), + "--autopilot.insecure", + fmt.Sprintf( + "--autopilot.address=localhost:%d", + n.autopilotServer.GetPort(), + ), } return n.newNode( diff --git a/litclient/jsoncallbacks.go b/litclient/jsoncallbacks.go index 49385d581..492876bf5 100644 --- a/litclient/jsoncallbacks.go +++ b/litclient/jsoncallbacks.go @@ -4,6 +4,7 @@ import ( "context" "github.com/lightninglabs/faraday/frdrpc" + "github.com/lightninglabs/lightning-terminal/litrpc" "github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/pool/poolrpc" "github.com/lightningnetwork/lnd/lnrpc" @@ -41,4 +42,8 @@ var Registrations = []StubPackageRegistration{ looprpc.RegisterSwapClientJSONCallbacks, poolrpc.RegisterTraderJSONCallbacks, frdrpc.RegisterFaradayServerJSONCallbacks, + litrpc.RegisterSessionsJSONCallbacks, + litrpc.RegisterAccountsJSONCallbacks, + litrpc.RegisterAutopilotJSONCallbacks, + litrpc.RegisterFirewallJSONCallbacks, } diff --git a/litrpc/Dockerfile b/litrpc/Dockerfile index 4d5ca15dd..b4c5112cd 100644 --- a/litrpc/Dockerfile +++ b/litrpc/Dockerfile @@ -12,12 +12,14 @@ ARG PROTOBUF_VERSION ARG GRPC_GATEWAY_VERSION ENV PROTOC_GEN_GO_GRPC_VERSION="v1.1.0" +ENV FALAFEL_VERSION="v0.9.1" RUN cd /tmp \ && go get google.golang.org/protobuf/cmd/protoc-gen-go@${PROTOBUF_VERSION} \ && go get google.golang.org/grpc/cmd/protoc-gen-go-grpc@${PROTOC_GEN_GO_GRPC_VERSION} \ && go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@${GRPC_GATEWAY_VERSION} \ - && go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@${GRPC_GATEWAY_VERSION} + && go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@${GRPC_GATEWAY_VERSION} \ + && go get github.com/lightninglabs/falafel@${FALAFEL_VERSION} WORKDIR /build diff --git a/litrpc/accounts.pb.json.go b/litrpc/accounts.pb.json.go new file mode 100644 index 000000000..5c8f724eb --- /dev/null +++ b/litrpc/accounts.pb.json.go @@ -0,0 +1,123 @@ +// Code generated by falafel 0.9.1. DO NOT EDIT. +// source: lit-accounts.proto + +package litrpc + +import ( + "context" + + gateway "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protojson" +) + +func RegisterAccountsJSONCallbacks(registry map[string]func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error))) { + + marshaler := &gateway.JSONPb{ + MarshalOptions: protojson.MarshalOptions{ + UseProtoNames: true, + EmitUnpopulated: true, + }, + } + + registry["litrpc.Accounts.CreateAccount"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &CreateAccountRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewAccountsClient(conn) + resp, err := client.CreateAccount(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + + registry["litrpc.Accounts.UpdateAccount"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &UpdateAccountRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewAccountsClient(conn) + resp, err := client.UpdateAccount(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + + registry["litrpc.Accounts.ListAccounts"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &ListAccountsRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewAccountsClient(conn) + resp, err := client.ListAccounts(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + + registry["litrpc.Accounts.RemoveAccount"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &RemoveAccountRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewAccountsClient(conn) + resp, err := client.RemoveAccount(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } +} diff --git a/litrpc/autopilot.pb.json.go b/litrpc/autopilot.pb.json.go new file mode 100644 index 000000000..76c915f39 --- /dev/null +++ b/litrpc/autopilot.pb.json.go @@ -0,0 +1,123 @@ +// Code generated by falafel 0.9.1. DO NOT EDIT. +// source: lit-autopilot.proto + +package litrpc + +import ( + "context" + + gateway "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protojson" +) + +func RegisterAutopilotJSONCallbacks(registry map[string]func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error))) { + + marshaler := &gateway.JSONPb{ + MarshalOptions: protojson.MarshalOptions{ + UseProtoNames: true, + EmitUnpopulated: true, + }, + } + + registry["litrpc.Autopilot.ListAutopilotFeatures"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &ListAutopilotFeaturesRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewAutopilotClient(conn) + resp, err := client.ListAutopilotFeatures(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + + registry["litrpc.Autopilot.AddAutopilotSession"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &AddAutopilotSessionRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewAutopilotClient(conn) + resp, err := client.AddAutopilotSession(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + + registry["litrpc.Autopilot.ListAutopilotSessions"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &ListAutopilotSessionsRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewAutopilotClient(conn) + resp, err := client.ListAutopilotSessions(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + + registry["litrpc.Autopilot.RevokeAutopilotSession"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &RevokeAutopilotSessionRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewAutopilotClient(conn) + resp, err := client.RevokeAutopilotSession(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } +} diff --git a/litrpc/firewall.pb.go b/litrpc/firewall.pb.go new file mode 100644 index 000000000..1aa42ac90 --- /dev/null +++ b/litrpc/firewall.pb.go @@ -0,0 +1,810 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.6.1 +// source: firewall.proto + +package litrpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ActionState int32 + +const ( + // + //No state was assigned to the action. This should never be the case. + ActionState_STATE_UNKNOWN ActionState = 0 + // + //Pending means that the request resulting in the action being created + //came through but that no response came back from the appropriate backend. + //This means that the Action is either still being processed or that it + //did not successfully complete. + ActionState_STATE_PENDING ActionState = 1 + // + //Done means that the action successfully completed. + ActionState_STATE_DONE ActionState = 2 + // + //Error means that the Action did not successfully complete. + ActionState_STATE_ERROR ActionState = 3 +) + +// Enum value maps for ActionState. +var ( + ActionState_name = map[int32]string{ + 0: "STATE_UNKNOWN", + 1: "STATE_PENDING", + 2: "STATE_DONE", + 3: "STATE_ERROR", + } + ActionState_value = map[string]int32{ + "STATE_UNKNOWN": 0, + "STATE_PENDING": 1, + "STATE_DONE": 2, + "STATE_ERROR": 3, + } +) + +func (x ActionState) Enum() *ActionState { + p := new(ActionState) + *p = x + return p +} + +func (x ActionState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ActionState) Descriptor() protoreflect.EnumDescriptor { + return file_firewall_proto_enumTypes[0].Descriptor() +} + +func (ActionState) Type() protoreflect.EnumType { + return &file_firewall_proto_enumTypes[0] +} + +func (x ActionState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ActionState.Descriptor instead. +func (ActionState) EnumDescriptor() ([]byte, []int) { + return file_firewall_proto_rawDescGZIP(), []int{0} +} + +type PrivacyMapConversionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //If set to true, then the input string will be taken as the real value and + //the response will the the pseudo value it if exists. Otherwise, the input + //string will be assumed to be the pseudo value. + RealToPseudo bool `protobuf:"varint,1,opt,name=real_to_pseudo,json=realToPseudo,proto3" json:"real_to_pseudo,omitempty"` + // + //The session ID under which to search for the real-pseudo pair. + SessionId []byte `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // + //The input to be converted into the real or pseudo value. + Input string `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` +} + +func (x *PrivacyMapConversionRequest) Reset() { + *x = PrivacyMapConversionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_firewall_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PrivacyMapConversionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrivacyMapConversionRequest) ProtoMessage() {} + +func (x *PrivacyMapConversionRequest) ProtoReflect() protoreflect.Message { + mi := &file_firewall_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PrivacyMapConversionRequest.ProtoReflect.Descriptor instead. +func (*PrivacyMapConversionRequest) Descriptor() ([]byte, []int) { + return file_firewall_proto_rawDescGZIP(), []int{0} +} + +func (x *PrivacyMapConversionRequest) GetRealToPseudo() bool { + if x != nil { + return x.RealToPseudo + } + return false +} + +func (x *PrivacyMapConversionRequest) GetSessionId() []byte { + if x != nil { + return x.SessionId + } + return nil +} + +func (x *PrivacyMapConversionRequest) GetInput() string { + if x != nil { + return x.Input + } + return "" +} + +type PrivacyMapConversionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The resulting real or pseudo output. + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` +} + +func (x *PrivacyMapConversionResponse) Reset() { + *x = PrivacyMapConversionResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_firewall_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PrivacyMapConversionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrivacyMapConversionResponse) ProtoMessage() {} + +func (x *PrivacyMapConversionResponse) ProtoReflect() protoreflect.Message { + mi := &file_firewall_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PrivacyMapConversionResponse.ProtoReflect.Descriptor instead. +func (*PrivacyMapConversionResponse) Descriptor() ([]byte, []int) { + return file_firewall_proto_rawDescGZIP(), []int{1} +} + +func (x *PrivacyMapConversionResponse) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +type ListActionsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The feature name which the filter the actions by. If left empty, all feature + //actions will be returned. + FeatureName string `protobuf:"bytes,1,opt,name=feature_name,json=featureName,proto3" json:"feature_name,omitempty"` + // + //The actor name to filter on. If left empty, all actor actions will be + //returned. + ActorName string `protobuf:"bytes,2,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // + //The method name to filter on. If left empty, actions for any method will be + //returned. + MethodName string `protobuf:"bytes,3,opt,name=method_name,json=methodName,proto3" json:"method_name,omitempty"` + // + //The action state to filter on. If set to zero, actions for any state will + //be returned. + State ActionState `protobuf:"varint,4,opt,name=state,proto3,enum=litrpc.ActionState" json:"state,omitempty"` + // + //The index of an action that will be used as the start of a query to + //determine which actions should be returned in the response. + IndexOffset uint64 `protobuf:"varint,5,opt,name=index_offset,json=indexOffset,proto3" json:"index_offset,omitempty"` + // + //The max number of actions to return in the response to this query. + MaxNumActions uint64 `protobuf:"varint,6,opt,name=max_num_actions,json=maxNumActions,proto3" json:"max_num_actions,omitempty"` + // + //If set, the actions returned will result from seeking backwards from the + //specified index offset. This can be used to paginate backwards. + Reversed bool `protobuf:"varint,7,opt,name=reversed,proto3" json:"reversed,omitempty"` + // + //Set to true if the total number of all actions that match the given filters + //should be counted and returned in the request. Note that setting this will + //significantly decrease the performance of the query if there are many + //actions in the db. + CountTotal bool `protobuf:"varint,8,opt,name=count_total,json=countTotal,proto3" json:"count_total,omitempty"` + // + //The session ID to filter on. If left empty, actions for any session will + //be returned. + SessionId []byte `protobuf:"bytes,9,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // + //If specified, then only actions created after the given timestamp will be + //considered. + StartTimestamp uint64 `protobuf:"varint,10,opt,name=start_timestamp,json=startTimestamp,proto3" json:"start_timestamp,omitempty"` + // + //If specified, then only actions created before the given timestamp will be + //considered. + EndTimestamp uint64 `protobuf:"varint,11,opt,name=end_timestamp,json=endTimestamp,proto3" json:"end_timestamp,omitempty"` +} + +func (x *ListActionsRequest) Reset() { + *x = ListActionsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_firewall_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListActionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListActionsRequest) ProtoMessage() {} + +func (x *ListActionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_firewall_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListActionsRequest.ProtoReflect.Descriptor instead. +func (*ListActionsRequest) Descriptor() ([]byte, []int) { + return file_firewall_proto_rawDescGZIP(), []int{2} +} + +func (x *ListActionsRequest) GetFeatureName() string { + if x != nil { + return x.FeatureName + } + return "" +} + +func (x *ListActionsRequest) GetActorName() string { + if x != nil { + return x.ActorName + } + return "" +} + +func (x *ListActionsRequest) GetMethodName() string { + if x != nil { + return x.MethodName + } + return "" +} + +func (x *ListActionsRequest) GetState() ActionState { + if x != nil { + return x.State + } + return ActionState_STATE_UNKNOWN +} + +func (x *ListActionsRequest) GetIndexOffset() uint64 { + if x != nil { + return x.IndexOffset + } + return 0 +} + +func (x *ListActionsRequest) GetMaxNumActions() uint64 { + if x != nil { + return x.MaxNumActions + } + return 0 +} + +func (x *ListActionsRequest) GetReversed() bool { + if x != nil { + return x.Reversed + } + return false +} + +func (x *ListActionsRequest) GetCountTotal() bool { + if x != nil { + return x.CountTotal + } + return false +} + +func (x *ListActionsRequest) GetSessionId() []byte { + if x != nil { + return x.SessionId + } + return nil +} + +func (x *ListActionsRequest) GetStartTimestamp() uint64 { + if x != nil { + return x.StartTimestamp + } + return 0 +} + +func (x *ListActionsRequest) GetEndTimestamp() uint64 { + if x != nil { + return x.EndTimestamp + } + return 0 +} + +type ListActionsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //A list of actions performed by the autopilot server. + Actions []*Action `protobuf:"bytes,1,rep,name=actions,proto3" json:"actions,omitempty"` + // + //The index of the last item in the set of returned actions. This can be used + //to seek further, pagination style. + LastIndexOffset uint64 `protobuf:"varint,2,opt,name=last_index_offset,json=lastIndexOffset,proto3" json:"last_index_offset,omitempty"` + // + //The total number of actions that matched the filter in the request. It is + //only set if count_total was set in the request. + TotalCount uint64 `protobuf:"varint,3,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` +} + +func (x *ListActionsResponse) Reset() { + *x = ListActionsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_firewall_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListActionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListActionsResponse) ProtoMessage() {} + +func (x *ListActionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_firewall_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListActionsResponse.ProtoReflect.Descriptor instead. +func (*ListActionsResponse) Descriptor() ([]byte, []int) { + return file_firewall_proto_rawDescGZIP(), []int{3} +} + +func (x *ListActionsResponse) GetActions() []*Action { + if x != nil { + return x.Actions + } + return nil +} + +func (x *ListActionsResponse) GetLastIndexOffset() uint64 { + if x != nil { + return x.LastIndexOffset + } + return 0 +} + +func (x *ListActionsResponse) GetTotalCount() uint64 { + if x != nil { + return x.TotalCount + } + return 0 +} + +type Action struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The name of the actor that initiated the action. + ActorName string `protobuf:"bytes,1,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // + //The name of the feature that triggered the action. + FeatureName string `protobuf:"bytes,2,opt,name=feature_name,json=featureName,proto3" json:"feature_name,omitempty"` + // + //A human readable reason that the action was performed. + Trigger string `protobuf:"bytes,3,opt,name=trigger,proto3" json:"trigger,omitempty"` + // + //A human readable string describing the intended outcome successfully + //performing the action. + Intent string `protobuf:"bytes,4,opt,name=intent,proto3" json:"intent,omitempty"` + // + //Structured info added by the action performer. + StructuredJsonData string `protobuf:"bytes,5,opt,name=structured_json_data,json=structuredJsonData,proto3" json:"structured_json_data,omitempty"` + // + //The URI of the method called. + RpcMethod string `protobuf:"bytes,6,opt,name=rpc_method,json=rpcMethod,proto3" json:"rpc_method,omitempty"` + // + //The parameters of the method call in compact json form. + RpcParamsJson string `protobuf:"bytes,7,opt,name=rpc_params_json,json=rpcParamsJson,proto3" json:"rpc_params_json,omitempty"` + // + //The unix timestamp in seconds at which the action was attempted. + Timestamp uint64 `protobuf:"varint,8,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // + //The action state. See ActionState for the meaning of each state. + State ActionState `protobuf:"varint,9,opt,name=state,proto3,enum=litrpc.ActionState" json:"state,omitempty"` + // + //If the state is Error, then this string will show the human readable reason + //for why the action errored out. + ErrorReason string `protobuf:"bytes,10,opt,name=error_reason,json=errorReason,proto3" json:"error_reason,omitempty"` + // + //The ID of the session under which the action was performed. + SessionId []byte `protobuf:"bytes,11,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` +} + +func (x *Action) Reset() { + *x = Action{} + if protoimpl.UnsafeEnabled { + mi := &file_firewall_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Action) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Action) ProtoMessage() {} + +func (x *Action) ProtoReflect() protoreflect.Message { + mi := &file_firewall_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Action.ProtoReflect.Descriptor instead. +func (*Action) Descriptor() ([]byte, []int) { + return file_firewall_proto_rawDescGZIP(), []int{4} +} + +func (x *Action) GetActorName() string { + if x != nil { + return x.ActorName + } + return "" +} + +func (x *Action) GetFeatureName() string { + if x != nil { + return x.FeatureName + } + return "" +} + +func (x *Action) GetTrigger() string { + if x != nil { + return x.Trigger + } + return "" +} + +func (x *Action) GetIntent() string { + if x != nil { + return x.Intent + } + return "" +} + +func (x *Action) GetStructuredJsonData() string { + if x != nil { + return x.StructuredJsonData + } + return "" +} + +func (x *Action) GetRpcMethod() string { + if x != nil { + return x.RpcMethod + } + return "" +} + +func (x *Action) GetRpcParamsJson() string { + if x != nil { + return x.RpcParamsJson + } + return "" +} + +func (x *Action) GetTimestamp() uint64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *Action) GetState() ActionState { + if x != nil { + return x.State + } + return ActionState_STATE_UNKNOWN +} + +func (x *Action) GetErrorReason() string { + if x != nil { + return x.ErrorReason + } + return "" +} + +func (x *Action) GetSessionId() []byte { + if x != nil { + return x.SessionId + } + return nil +} + +var File_firewall_proto protoreflect.FileDescriptor + +var file_firewall_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x06, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x22, 0x78, 0x0a, 0x1b, 0x50, 0x72, 0x69, 0x76, + 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x72, 0x65, 0x61, 0x6c, 0x5f, + 0x74, 0x6f, 0x5f, 0x70, 0x73, 0x65, 0x75, 0x64, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0c, 0x72, 0x65, 0x61, 0x6c, 0x54, 0x6f, 0x50, 0x73, 0x65, 0x75, 0x64, 0x6f, 0x12, 0x1d, 0x0a, + 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, + 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, + 0x75, 0x74, 0x22, 0x36, 0x0a, 0x1c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, + 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x9f, 0x03, 0x0a, 0x12, 0x4c, + 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x4f, 0x66, 0x66, 0x73, + 0x65, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x61, 0x78, 0x5f, 0x6e, 0x75, 0x6d, 0x5f, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6d, 0x61, 0x78, + 0x4e, 0x75, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, + 0x76, 0x65, 0x72, 0x73, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, + 0x76, 0x65, 0x72, 0x73, 0x65, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x0f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x42, + 0x02, 0x30, 0x01, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x12, 0x27, 0x0a, 0x0d, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0c, + 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x8c, 0x01, 0x0a, + 0x13, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2a, + 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x6f, 0x66, 0x66, + 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x84, 0x03, 0x0a, 0x06, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x74, 0x6f, + 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x65, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x72, 0x69, 0x67, + 0x67, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, + 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x69, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x74, + 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x64, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x5f, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, + 0x75, 0x72, 0x65, 0x64, 0x4a, 0x73, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, + 0x72, 0x70, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x72, 0x70, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x72, + 0x70, 0x63, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x70, 0x63, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x4a, + 0x73, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x29, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x49, 0x64, 0x2a, 0x54, 0x0a, 0x0b, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, + 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45, + 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x54, 0x41, 0x54, 0x45, + 0x5f, 0x44, 0x4f, 0x4e, 0x45, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x54, 0x45, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x32, 0xb5, 0x01, 0x0a, 0x08, 0x46, 0x69, 0x72, + 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x12, 0x46, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, 0x0a, + 0x14, 0x50, 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, 0x43, 0x6f, 0x6e, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, + 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6c, 0x69, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, 0x43, 0x6f, + 0x6e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, + 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x69, 0x67, + 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x2f, + 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_firewall_proto_rawDescOnce sync.Once + file_firewall_proto_rawDescData = file_firewall_proto_rawDesc +) + +func file_firewall_proto_rawDescGZIP() []byte { + file_firewall_proto_rawDescOnce.Do(func() { + file_firewall_proto_rawDescData = protoimpl.X.CompressGZIP(file_firewall_proto_rawDescData) + }) + return file_firewall_proto_rawDescData +} + +var file_firewall_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_firewall_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_firewall_proto_goTypes = []interface{}{ + (ActionState)(0), // 0: litrpc.ActionState + (*PrivacyMapConversionRequest)(nil), // 1: litrpc.PrivacyMapConversionRequest + (*PrivacyMapConversionResponse)(nil), // 2: litrpc.PrivacyMapConversionResponse + (*ListActionsRequest)(nil), // 3: litrpc.ListActionsRequest + (*ListActionsResponse)(nil), // 4: litrpc.ListActionsResponse + (*Action)(nil), // 5: litrpc.Action +} +var file_firewall_proto_depIdxs = []int32{ + 0, // 0: litrpc.ListActionsRequest.state:type_name -> litrpc.ActionState + 5, // 1: litrpc.ListActionsResponse.actions:type_name -> litrpc.Action + 0, // 2: litrpc.Action.state:type_name -> litrpc.ActionState + 3, // 3: litrpc.Firewall.ListActions:input_type -> litrpc.ListActionsRequest + 1, // 4: litrpc.Firewall.PrivacyMapConversion:input_type -> litrpc.PrivacyMapConversionRequest + 4, // 5: litrpc.Firewall.ListActions:output_type -> litrpc.ListActionsResponse + 2, // 6: litrpc.Firewall.PrivacyMapConversion:output_type -> litrpc.PrivacyMapConversionResponse + 5, // [5:7] is the sub-list for method output_type + 3, // [3:5] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_firewall_proto_init() } +func file_firewall_proto_init() { + if File_firewall_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_firewall_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PrivacyMapConversionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_firewall_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PrivacyMapConversionResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_firewall_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListActionsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_firewall_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListActionsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_firewall_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Action); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_firewall_proto_rawDesc, + NumEnums: 1, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_firewall_proto_goTypes, + DependencyIndexes: file_firewall_proto_depIdxs, + EnumInfos: file_firewall_proto_enumTypes, + MessageInfos: file_firewall_proto_msgTypes, + }.Build() + File_firewall_proto = out.File + file_firewall_proto_rawDesc = nil + file_firewall_proto_goTypes = nil + file_firewall_proto_depIdxs = nil +} diff --git a/litrpc/firewall.pb.json.go b/litrpc/firewall.pb.json.go new file mode 100644 index 000000000..faafa22e7 --- /dev/null +++ b/litrpc/firewall.pb.json.go @@ -0,0 +1,73 @@ +// Code generated by falafel 0.9.1. DO NOT EDIT. +// source: firewall.proto + +package litrpc + +import ( + "context" + + gateway "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protojson" +) + +func RegisterFirewallJSONCallbacks(registry map[string]func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error))) { + + marshaler := &gateway.JSONPb{ + MarshalOptions: protojson.MarshalOptions{ + UseProtoNames: true, + EmitUnpopulated: true, + }, + } + + registry["litrpc.Firewall.ListActions"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &ListActionsRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewFirewallClient(conn) + resp, err := client.ListActions(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + + registry["litrpc.Firewall.PrivacyMapConversion"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &PrivacyMapConversionRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewFirewallClient(conn) + resp, err := client.PrivacyMapConversion(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } +} diff --git a/litrpc/firewall.proto b/litrpc/firewall.proto new file mode 100644 index 000000000..8c5b18e88 --- /dev/null +++ b/litrpc/firewall.proto @@ -0,0 +1,209 @@ +syntax = "proto3"; + +package litrpc; + +option go_package = "github.com/lightninglabs/lightning-terminal/litrpc"; + +service Firewall { + rpc ListActions (ListActionsRequest) returns (ListActionsResponse); + rpc PrivacyMapConversion (PrivacyMapConversionRequest) + returns (PrivacyMapConversionResponse); +} + +message PrivacyMapConversionRequest { + /* + If set to true, then the input string will be taken as the real value and + the response will the the pseudo value it if exists. Otherwise, the input + string will be assumed to be the pseudo value. + */ + bool real_to_pseudo = 1; + + /* + The session ID under which to search for the real-pseudo pair. + */ + bytes session_id = 2; + + /* + The input to be converted into the real or pseudo value. + */ + string input = 3; +} + +message PrivacyMapConversionResponse { + /* + The resulting real or pseudo output. + */ + string output = 1; +} + +message ListActionsRequest { + /* + The feature name which the filter the actions by. If left empty, all feature + actions will be returned. + */ + string feature_name = 1; + + /* + The actor name to filter on. If left empty, all actor actions will be + returned. + */ + string actor_name = 2; + + /* + The method name to filter on. If left empty, actions for any method will be + returned. + */ + string method_name = 3; + + /* + The action state to filter on. If set to zero, actions for any state will + be returned. + */ + ActionState state = 4; + + /* + The index of an action that will be used as the start of a query to + determine which actions should be returned in the response. + */ + uint64 index_offset = 5; + + /* + The max number of actions to return in the response to this query. + */ + uint64 max_num_actions = 6; + + /* + If set, the actions returned will result from seeking backwards from the + specified index offset. This can be used to paginate backwards. + */ + bool reversed = 7; + + /* + Set to true if the total number of all actions that match the given filters + should be counted and returned in the request. Note that setting this will + significantly decrease the performance of the query if there are many + actions in the db. + */ + bool count_total = 8; + + /* + The session ID to filter on. If left empty, actions for any session will + be returned. + */ + bytes session_id = 9; + + /* + If specified, then only actions created after the given timestamp will be + considered. + */ + uint64 start_timestamp = 10 [jstype = JS_STRING]; + + /* + If specified, then only actions created before the given timestamp will be + considered. + */ + uint64 end_timestamp = 11 [jstype = JS_STRING]; +} + +message ListActionsResponse { + /* + A list of actions performed by the autopilot server. + */ + repeated Action actions = 1; + + /* + The index of the last item in the set of returned actions. This can be used + to seek further, pagination style. + */ + uint64 last_index_offset = 2; + + /* + The total number of actions that matched the filter in the request. It is + only set if count_total was set in the request. + */ + uint64 total_count = 3; +} + +message Action { + /* + The name of the actor that initiated the action. + */ + string actor_name = 1; + + /* + The name of the feature that triggered the action. + */ + string feature_name = 2; + + /* + A human readable reason that the action was performed. + */ + string trigger = 3; + + /* + A human readable string describing the intended outcome successfully + performing the action. + */ + string intent = 4; + + /* + Structured info added by the action performer. + */ + string structured_json_data = 5; + + /* + The URI of the method called. + */ + string rpc_method = 6; + + /* + The parameters of the method call in compact json form. + */ + string rpc_params_json = 7; + + /* + The unix timestamp in seconds at which the action was attempted. + */ + uint64 timestamp = 8 [jstype = JS_STRING]; + + /* + The action state. See ActionState for the meaning of each state. + */ + ActionState state = 9; + + /* + If the state is Error, then this string will show the human readable reason + for why the action errored out. + */ + string error_reason = 10; + + /* + The ID of the session under which the action was performed. + */ + bytes session_id = 11; +} + +enum ActionState { + /* + No state was assigned to the action. This should never be the case. + */ + STATE_UNKNOWN = 0; + + /* + Pending means that the request resulting in the action being created + came through but that no response came back from the appropriate backend. + This means that the Action is either still being processed or that it + did not successfully complete. + */ + STATE_PENDING = 1; + + /* + Done means that the action successfully completed. + */ + STATE_DONE = 2; + + /* + Error means that the Action did not successfully complete. + */ + STATE_ERROR = 3; +} \ No newline at end of file diff --git a/litrpc/firewall_grpc.pb.go b/litrpc/firewall_grpc.pb.go new file mode 100644 index 000000000..78c05e5b8 --- /dev/null +++ b/litrpc/firewall_grpc.pb.go @@ -0,0 +1,137 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package litrpc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// FirewallClient is the client API for Firewall service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type FirewallClient interface { + ListActions(ctx context.Context, in *ListActionsRequest, opts ...grpc.CallOption) (*ListActionsResponse, error) + PrivacyMapConversion(ctx context.Context, in *PrivacyMapConversionRequest, opts ...grpc.CallOption) (*PrivacyMapConversionResponse, error) +} + +type firewallClient struct { + cc grpc.ClientConnInterface +} + +func NewFirewallClient(cc grpc.ClientConnInterface) FirewallClient { + return &firewallClient{cc} +} + +func (c *firewallClient) ListActions(ctx context.Context, in *ListActionsRequest, opts ...grpc.CallOption) (*ListActionsResponse, error) { + out := new(ListActionsResponse) + err := c.cc.Invoke(ctx, "/litrpc.Firewall/ListActions", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *firewallClient) PrivacyMapConversion(ctx context.Context, in *PrivacyMapConversionRequest, opts ...grpc.CallOption) (*PrivacyMapConversionResponse, error) { + out := new(PrivacyMapConversionResponse) + err := c.cc.Invoke(ctx, "/litrpc.Firewall/PrivacyMapConversion", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// FirewallServer is the server API for Firewall service. +// All implementations must embed UnimplementedFirewallServer +// for forward compatibility +type FirewallServer interface { + ListActions(context.Context, *ListActionsRequest) (*ListActionsResponse, error) + PrivacyMapConversion(context.Context, *PrivacyMapConversionRequest) (*PrivacyMapConversionResponse, error) + mustEmbedUnimplementedFirewallServer() +} + +// UnimplementedFirewallServer must be embedded to have forward compatible implementations. +type UnimplementedFirewallServer struct { +} + +func (UnimplementedFirewallServer) ListActions(context.Context, *ListActionsRequest) (*ListActionsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListActions not implemented") +} +func (UnimplementedFirewallServer) PrivacyMapConversion(context.Context, *PrivacyMapConversionRequest) (*PrivacyMapConversionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method PrivacyMapConversion not implemented") +} +func (UnimplementedFirewallServer) mustEmbedUnimplementedFirewallServer() {} + +// UnsafeFirewallServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to FirewallServer will +// result in compilation errors. +type UnsafeFirewallServer interface { + mustEmbedUnimplementedFirewallServer() +} + +func RegisterFirewallServer(s grpc.ServiceRegistrar, srv FirewallServer) { + s.RegisterService(&Firewall_ServiceDesc, srv) +} + +func _Firewall_ListActions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListActionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(FirewallServer).ListActions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/litrpc.Firewall/ListActions", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(FirewallServer).ListActions(ctx, req.(*ListActionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Firewall_PrivacyMapConversion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PrivacyMapConversionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(FirewallServer).PrivacyMapConversion(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/litrpc.Firewall/PrivacyMapConversion", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(FirewallServer).PrivacyMapConversion(ctx, req.(*PrivacyMapConversionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Firewall_ServiceDesc is the grpc.ServiceDesc for Firewall service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Firewall_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "litrpc.Firewall", + HandlerType: (*FirewallServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListActions", + Handler: _Firewall_ListActions_Handler, + }, + { + MethodName: "PrivacyMapConversion", + Handler: _Firewall_PrivacyMapConversion_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "firewall.proto", +} diff --git a/litrpc/gen_protos.sh b/litrpc/gen_protos.sh index cc413d8c8..d0de7d16e 100755 --- a/litrpc/gen_protos.sh +++ b/litrpc/gen_protos.sh @@ -4,12 +4,27 @@ set -e # generate compiles the *.pb.go stubs from the *.proto files. function generate() { - # Generate the gRPC bindings for all proto files. for file in ./*.proto; do - protoc -I/usr/local/include -I. -I.. \ + # Generate the gRPC bindings for all proto files. + protoc -I. -I.. \ --go_out . --go_opt paths=source_relative \ --go-grpc_out . --go-grpc_opt paths=source_relative \ "${file}" + + # Only generate JSON/WASM stubs if requested. + if [[ "$1" == "no-wasm" ]]; then + return + fi + + # Generate the JSON/WASM autopilot stubs. + falafel=$(which falafel) + pkg="litrpc" + opts="package_name=$pkg,js_stubs=1" + protoc -I. -I.. \ + --plugin=protoc-gen-custom=$falafel\ + --custom_out=. \ + --custom_opt="$opts" \ + "${file}" done } @@ -23,3 +38,8 @@ pushd litrpc format generate popd + +pushd autopilotserverrpc +format +generate no-wasm +popd \ No newline at end of file diff --git a/litrpc/interface.go b/litrpc/interface.go new file mode 100644 index 000000000..e4391ddf6 --- /dev/null +++ b/litrpc/interface.go @@ -0,0 +1,63 @@ +package litrpc + +import ( + "github.com/lightninglabs/faraday/frdrpc" + "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/pool/poolrpc" + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/grpc" +) + +// LitdClient is an interface that can be used to access all the subservers +// of Litd. +type LitdClient interface { + // Lnd returns an lnrpc.LightingClient implementation. + Lnd() lnrpc.LightningClient + + // Loop returns a looprpc.SwapClientClient implementation. + Loop() looprpc.SwapClientClient + + // Pool returns a poolrpc.TraderClient implementation. + Pool() poolrpc.TraderClient + + // Faraday returns a frdrpc.FaradayServerClient implementation. + Faraday() frdrpc.FaradayServerClient +} + +// client is an implementation of the LitdClient. +type client struct { + lnd lnrpc.LightningClient + loop looprpc.SwapClientClient + pool poolrpc.TraderClient + faraday frdrpc.FaradayServerClient +} + +// Lnd returns an lnrpc.LightingClient implementation. +func (c *client) Lnd() lnrpc.LightningClient { + return c.lnd +} + +// Loop returns a looprpc.SwapClientClient implementation. +func (c *client) Loop() looprpc.SwapClientClient { + return c.loop +} + +// Pool returns a poolrpc.TraderClient implementation. +func (c *client) Pool() poolrpc.TraderClient { + return c.pool +} + +// Faraday returns a frdrpc.FaradayServerClient implementation. +func (c *client) Faraday() frdrpc.FaradayServerClient { + return c.faraday +} + +// NewLitdClient constructs a new LitdClient from the passed grpc ClientConn. +func NewLitdClient(cc grpc.ClientConnInterface) LitdClient { + return &client{ + lnd: lnrpc.NewLightningClient(cc), + loop: looprpc.NewSwapClientClient(cc), + pool: poolrpc.NewTraderClient(cc), + faraday: frdrpc.NewFaradayServerClient(cc), + } +} diff --git a/litrpc/lit-accounts.pb.go b/litrpc/lit-accounts.pb.go index ccc5371a1..5d203eaae 100644 --- a/litrpc/lit-accounts.pb.go +++ b/litrpc/lit-accounts.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 +// protoc-gen-go v1.28.1 // protoc v3.6.1 // source: lit-accounts.proto diff --git a/litrpc/lit-autopilot.pb.go b/litrpc/lit-autopilot.pb.go new file mode 100644 index 000000000..67d324634 --- /dev/null +++ b/litrpc/lit-autopilot.pb.go @@ -0,0 +1,1111 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.6.1 +// source: lit-autopilot.proto + +package litrpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AddAutopilotSessionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //A human readable label to assign to the session. + Label string `protobuf:"bytes,1,opt,name=label,proto3" json:"label,omitempty"` + // + //The unix timestamp at which this session should be revoked. + ExpiryTimestampSeconds uint64 `protobuf:"varint,2,opt,name=expiry_timestamp_seconds,json=expiryTimestampSeconds,proto3" json:"expiry_timestamp_seconds,omitempty"` + // + //The address of the mailbox server to connect to for this session. + MailboxServerAddr string `protobuf:"bytes,3,opt,name=mailbox_server_addr,json=mailboxServerAddr,proto3" json:"mailbox_server_addr,omitempty"` + // + //Set to true if tls should be skipped for when connecting to the mailbox. + DevServer bool `protobuf:"varint,4,opt,name=dev_server,json=devServer,proto3" json:"dev_server,omitempty"` + // + //The features that the session should subscribe to. Each feature maps to + //a FeatureConfig that should be applied to that feature. + Features map[string]*FeatureConfig `protobuf:"bytes,5,rep,name=features,proto3" json:"features,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // + //Rules that apply to the entire session. By default, no rules will apply + //to the entire session. + SessionRules *RulesMap `protobuf:"bytes,6,opt,name=session_rules,json=sessionRules,proto3" json:"session_rules,omitempty"` + // + //Set to true of the session should not make use of the privacy mapper. + NoPrivacyMapper bool `protobuf:"varint,7,opt,name=no_privacy_mapper,json=noPrivacyMapper,proto3" json:"no_privacy_mapper,omitempty"` +} + +func (x *AddAutopilotSessionRequest) Reset() { + *x = AddAutopilotSessionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_autopilot_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddAutopilotSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddAutopilotSessionRequest) ProtoMessage() {} + +func (x *AddAutopilotSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_lit_autopilot_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddAutopilotSessionRequest.ProtoReflect.Descriptor instead. +func (*AddAutopilotSessionRequest) Descriptor() ([]byte, []int) { + return file_lit_autopilot_proto_rawDescGZIP(), []int{0} +} + +func (x *AddAutopilotSessionRequest) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *AddAutopilotSessionRequest) GetExpiryTimestampSeconds() uint64 { + if x != nil { + return x.ExpiryTimestampSeconds + } + return 0 +} + +func (x *AddAutopilotSessionRequest) GetMailboxServerAddr() string { + if x != nil { + return x.MailboxServerAddr + } + return "" +} + +func (x *AddAutopilotSessionRequest) GetDevServer() bool { + if x != nil { + return x.DevServer + } + return false +} + +func (x *AddAutopilotSessionRequest) GetFeatures() map[string]*FeatureConfig { + if x != nil { + return x.Features + } + return nil +} + +func (x *AddAutopilotSessionRequest) GetSessionRules() *RulesMap { + if x != nil { + return x.SessionRules + } + return nil +} + +func (x *AddAutopilotSessionRequest) GetNoPrivacyMapper() bool { + if x != nil { + return x.NoPrivacyMapper + } + return false +} + +type FeatureConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The RulesMap acts as an override map. In other words, by default the rules + //values recommended by the Auto Pilot server will be used but the RulesMap + //can be used to override the defaults. + Rules *RulesMap `protobuf:"bytes,1,opt,name=rules,proto3" json:"rules,omitempty"` + // + //Serialised configuration for the feature. + Config []byte `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` +} + +func (x *FeatureConfig) Reset() { + *x = FeatureConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_autopilot_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FeatureConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FeatureConfig) ProtoMessage() {} + +func (x *FeatureConfig) ProtoReflect() protoreflect.Message { + mi := &file_lit_autopilot_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FeatureConfig.ProtoReflect.Descriptor instead. +func (*FeatureConfig) Descriptor() ([]byte, []int) { + return file_lit_autopilot_proto_rawDescGZIP(), []int{1} +} + +func (x *FeatureConfig) GetRules() *RulesMap { + if x != nil { + return x.Rules + } + return nil +} + +func (x *FeatureConfig) GetConfig() []byte { + if x != nil { + return x.Config + } + return nil +} + +type ListAutopilotSessionsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ListAutopilotSessionsRequest) Reset() { + *x = ListAutopilotSessionsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_autopilot_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListAutopilotSessionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAutopilotSessionsRequest) ProtoMessage() {} + +func (x *ListAutopilotSessionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_lit_autopilot_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAutopilotSessionsRequest.ProtoReflect.Descriptor instead. +func (*ListAutopilotSessionsRequest) Descriptor() ([]byte, []int) { + return file_lit_autopilot_proto_rawDescGZIP(), []int{2} +} + +type ListAutopilotSessionsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //A list of the Autopilot sessions. + Sessions []*Session `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"` +} + +func (x *ListAutopilotSessionsResponse) Reset() { + *x = ListAutopilotSessionsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_autopilot_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListAutopilotSessionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAutopilotSessionsResponse) ProtoMessage() {} + +func (x *ListAutopilotSessionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_lit_autopilot_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAutopilotSessionsResponse.ProtoReflect.Descriptor instead. +func (*ListAutopilotSessionsResponse) Descriptor() ([]byte, []int) { + return file_lit_autopilot_proto_rawDescGZIP(), []int{3} +} + +func (x *ListAutopilotSessionsResponse) GetSessions() []*Session { + if x != nil { + return x.Sessions + } + return nil +} + +type AddAutopilotSessionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //Details of the session that was just created. + Session *Session `protobuf:"bytes,1,opt,name=session,proto3" json:"session,omitempty"` +} + +func (x *AddAutopilotSessionResponse) Reset() { + *x = AddAutopilotSessionResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_autopilot_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddAutopilotSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddAutopilotSessionResponse) ProtoMessage() {} + +func (x *AddAutopilotSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_lit_autopilot_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddAutopilotSessionResponse.ProtoReflect.Descriptor instead. +func (*AddAutopilotSessionResponse) Descriptor() ([]byte, []int) { + return file_lit_autopilot_proto_rawDescGZIP(), []int{4} +} + +func (x *AddAutopilotSessionResponse) GetSession() *Session { + if x != nil { + return x.Session + } + return nil +} + +type ListAutopilotFeaturesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ListAutopilotFeaturesRequest) Reset() { + *x = ListAutopilotFeaturesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_autopilot_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListAutopilotFeaturesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAutopilotFeaturesRequest) ProtoMessage() {} + +func (x *ListAutopilotFeaturesRequest) ProtoReflect() protoreflect.Message { + mi := &file_lit_autopilot_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAutopilotFeaturesRequest.ProtoReflect.Descriptor instead. +func (*ListAutopilotFeaturesRequest) Descriptor() ([]byte, []int) { + return file_lit_autopilot_proto_rawDescGZIP(), []int{5} +} + +type ListAutopilotFeaturesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //A map of feature names to Feature objects describing the feature. + Features map[string]*Feature `protobuf:"bytes,1,rep,name=features,proto3" json:"features,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *ListAutopilotFeaturesResponse) Reset() { + *x = ListAutopilotFeaturesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_autopilot_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListAutopilotFeaturesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAutopilotFeaturesResponse) ProtoMessage() {} + +func (x *ListAutopilotFeaturesResponse) ProtoReflect() protoreflect.Message { + mi := &file_lit_autopilot_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAutopilotFeaturesResponse.ProtoReflect.Descriptor instead. +func (*ListAutopilotFeaturesResponse) Descriptor() ([]byte, []int) { + return file_lit_autopilot_proto_rawDescGZIP(), []int{6} +} + +func (x *ListAutopilotFeaturesResponse) GetFeatures() map[string]*Feature { + if x != nil { + return x.Features + } + return nil +} + +type RevokeAutopilotSessionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + LocalPublicKey []byte `protobuf:"bytes,1,opt,name=local_public_key,json=localPublicKey,proto3" json:"local_public_key,omitempty"` +} + +func (x *RevokeAutopilotSessionRequest) Reset() { + *x = RevokeAutopilotSessionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_autopilot_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RevokeAutopilotSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeAutopilotSessionRequest) ProtoMessage() {} + +func (x *RevokeAutopilotSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_lit_autopilot_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeAutopilotSessionRequest.ProtoReflect.Descriptor instead. +func (*RevokeAutopilotSessionRequest) Descriptor() ([]byte, []int) { + return file_lit_autopilot_proto_rawDescGZIP(), []int{7} +} + +func (x *RevokeAutopilotSessionRequest) GetLocalPublicKey() []byte { + if x != nil { + return x.LocalPublicKey + } + return nil +} + +type RevokeAutopilotSessionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *RevokeAutopilotSessionResponse) Reset() { + *x = RevokeAutopilotSessionResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_autopilot_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RevokeAutopilotSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeAutopilotSessionResponse) ProtoMessage() {} + +func (x *RevokeAutopilotSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_lit_autopilot_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeAutopilotSessionResponse.ProtoReflect.Descriptor instead. +func (*RevokeAutopilotSessionResponse) Descriptor() ([]byte, []int) { + return file_lit_autopilot_proto_rawDescGZIP(), []int{8} +} + +type Feature struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //Name is the name of the Autopilot feature. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // + //A human readable description of what the feature offers. + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // + //A map of rules that make sense for this feature. Each rule is accompanied + //with appropriate default values for the feature along with minimum and + //maximum values for the rules. + Rules map[string]*RuleValues `protobuf:"bytes,3,rep,name=rules,proto3" json:"rules,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // + //A list of URI permissions required by the feature. + PermissionsList []*Permissions `protobuf:"bytes,4,rep,name=permissions_list,json=permissionsList,proto3" json:"permissions_list,omitempty"` + // + //A boolean indicating if the user would need to upgrade their Litd version in + //order to subscribe to the Autopilot feature. This will be true if the + //feature rules set contains a rule that Litd is unaware of. + RequiresUpgrade bool `protobuf:"varint,5,opt,name=requires_upgrade,json=requiresUpgrade,proto3" json:"requires_upgrade,omitempty"` +} + +func (x *Feature) Reset() { + *x = Feature{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_autopilot_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Feature) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Feature) ProtoMessage() {} + +func (x *Feature) ProtoReflect() protoreflect.Message { + mi := &file_lit_autopilot_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Feature.ProtoReflect.Descriptor instead. +func (*Feature) Descriptor() ([]byte, []int) { + return file_lit_autopilot_proto_rawDescGZIP(), []int{9} +} + +func (x *Feature) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Feature) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Feature) GetRules() map[string]*RuleValues { + if x != nil { + return x.Rules + } + return nil +} + +func (x *Feature) GetPermissionsList() []*Permissions { + if x != nil { + return x.PermissionsList + } + return nil +} + +func (x *Feature) GetRequiresUpgrade() bool { + if x != nil { + return x.RequiresUpgrade + } + return false +} + +type RuleValues struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //Whether or not the users version of Litd is aware of this rule. + Known bool `protobuf:"varint,1,opt,name=known,proto3" json:"known,omitempty"` + // + //The default values for the rule that the Autopilot server recommends for + //the associated feature. + Defaults *RuleValue `protobuf:"bytes,2,opt,name=defaults,proto3" json:"defaults,omitempty"` + // + //The minimum sane value for this rule for the associated feature. + MinValue *RuleValue `protobuf:"bytes,3,opt,name=min_value,json=minValue,proto3" json:"min_value,omitempty"` + // + //The maximum sane value for this rule for the associated feature. + MaxValue *RuleValue `protobuf:"bytes,4,opt,name=max_value,json=maxValue,proto3" json:"max_value,omitempty"` +} + +func (x *RuleValues) Reset() { + *x = RuleValues{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_autopilot_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RuleValues) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RuleValues) ProtoMessage() {} + +func (x *RuleValues) ProtoReflect() protoreflect.Message { + mi := &file_lit_autopilot_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RuleValues.ProtoReflect.Descriptor instead. +func (*RuleValues) Descriptor() ([]byte, []int) { + return file_lit_autopilot_proto_rawDescGZIP(), []int{10} +} + +func (x *RuleValues) GetKnown() bool { + if x != nil { + return x.Known + } + return false +} + +func (x *RuleValues) GetDefaults() *RuleValue { + if x != nil { + return x.Defaults + } + return nil +} + +func (x *RuleValues) GetMinValue() *RuleValue { + if x != nil { + return x.MinValue + } + return nil +} + +func (x *RuleValues) GetMaxValue() *RuleValue { + if x != nil { + return x.MaxValue + } + return nil +} + +type Permissions struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The URI in question. + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + // + //A list of the permissions required for this method. + Operations []*MacaroonPermission `protobuf:"bytes,2,rep,name=operations,proto3" json:"operations,omitempty"` +} + +func (x *Permissions) Reset() { + *x = Permissions{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_autopilot_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Permissions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Permissions) ProtoMessage() {} + +func (x *Permissions) ProtoReflect() protoreflect.Message { + mi := &file_lit_autopilot_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Permissions.ProtoReflect.Descriptor instead. +func (*Permissions) Descriptor() ([]byte, []int) { + return file_lit_autopilot_proto_rawDescGZIP(), []int{11} +} + +func (x *Permissions) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *Permissions) GetOperations() []*MacaroonPermission { + if x != nil { + return x.Operations + } + return nil +} + +var File_lit_autopilot_proto protoreflect.FileDescriptor + +var file_lit_autopilot_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x6c, 0x69, 0x74, 0x2d, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x1a, 0x12, 0x6c, + 0x69, 0x74, 0x2d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0xc4, 0x03, 0x0a, 0x1a, 0x41, 0x64, 0x64, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, + 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x3c, 0x0a, 0x18, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, + 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x16, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x53, 0x65, 0x63, + 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x5f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x11, 0x6d, 0x61, 0x69, 0x6c, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x41, 0x64, 0x64, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x76, 0x5f, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x64, 0x65, 0x76, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, + 0x64, 0x64, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x73, 0x12, 0x35, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x75, 0x6c, + 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x0c, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x6e, 0x6f, 0x5f, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x65, 0x72, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6e, 0x6f, 0x50, 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x4d, 0x61, + 0x70, 0x70, 0x65, 0x72, 0x1a, 0x52, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x4f, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x26, 0x0a, 0x05, 0x72, 0x75, 0x6c, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x1e, 0x0a, 0x1c, 0x4c, 0x69, 0x73, + 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4c, 0x0a, 0x1d, 0x4c, 0x69, 0x73, + 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x08, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, + 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x48, 0x0a, 0x1b, 0x41, 0x64, 0x64, 0x41, 0x75, + 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x22, 0x1e, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, + 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0xbe, 0x01, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, + 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x65, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x73, 0x1a, 0x4c, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x25, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0x49, 0x0a, 0x1d, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, 0x74, 0x6f, + 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x6c, + 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, + 0x1e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0xaa, 0x02, 0x0a, 0x07, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x30, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x72, 0x75, + 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x10, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x0f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x4c, + 0x69, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x5f, + 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x72, + 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x1a, 0x4c, + 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb1, 0x01, 0x0a, + 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6b, + 0x6e, 0x6f, 0x77, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x6b, 0x6e, 0x6f, 0x77, + 0x6e, 0x12, 0x2d, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, + 0x12, 0x2e, 0x0a, 0x09, 0x6d, 0x69, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6d, 0x69, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x2e, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x22, 0x61, 0x0a, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x3a, 0x0a, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x69, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, + 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x32, 0xa0, 0x03, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, + 0x74, 0x12, 0x64, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, + 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x24, 0x2e, 0x6c, 0x69, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, + 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x25, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, + 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x13, 0x41, 0x64, 0x64, 0x41, 0x75, + 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, + 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x75, 0x74, 0x6f, 0x70, + 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, + 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x24, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, + 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x67, 0x0a, + 0x16, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, + 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, + 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, + 0x62, 0x73, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x65, 0x72, + 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_lit_autopilot_proto_rawDescOnce sync.Once + file_lit_autopilot_proto_rawDescData = file_lit_autopilot_proto_rawDesc +) + +func file_lit_autopilot_proto_rawDescGZIP() []byte { + file_lit_autopilot_proto_rawDescOnce.Do(func() { + file_lit_autopilot_proto_rawDescData = protoimpl.X.CompressGZIP(file_lit_autopilot_proto_rawDescData) + }) + return file_lit_autopilot_proto_rawDescData +} + +var file_lit_autopilot_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_lit_autopilot_proto_goTypes = []interface{}{ + (*AddAutopilotSessionRequest)(nil), // 0: litrpc.AddAutopilotSessionRequest + (*FeatureConfig)(nil), // 1: litrpc.FeatureConfig + (*ListAutopilotSessionsRequest)(nil), // 2: litrpc.ListAutopilotSessionsRequest + (*ListAutopilotSessionsResponse)(nil), // 3: litrpc.ListAutopilotSessionsResponse + (*AddAutopilotSessionResponse)(nil), // 4: litrpc.AddAutopilotSessionResponse + (*ListAutopilotFeaturesRequest)(nil), // 5: litrpc.ListAutopilotFeaturesRequest + (*ListAutopilotFeaturesResponse)(nil), // 6: litrpc.ListAutopilotFeaturesResponse + (*RevokeAutopilotSessionRequest)(nil), // 7: litrpc.RevokeAutopilotSessionRequest + (*RevokeAutopilotSessionResponse)(nil), // 8: litrpc.RevokeAutopilotSessionResponse + (*Feature)(nil), // 9: litrpc.Feature + (*RuleValues)(nil), // 10: litrpc.RuleValues + (*Permissions)(nil), // 11: litrpc.Permissions + nil, // 12: litrpc.AddAutopilotSessionRequest.FeaturesEntry + nil, // 13: litrpc.ListAutopilotFeaturesResponse.FeaturesEntry + nil, // 14: litrpc.Feature.RulesEntry + (*RulesMap)(nil), // 15: litrpc.RulesMap + (*Session)(nil), // 16: litrpc.Session + (*RuleValue)(nil), // 17: litrpc.RuleValue + (*MacaroonPermission)(nil), // 18: litrpc.MacaroonPermission +} +var file_lit_autopilot_proto_depIdxs = []int32{ + 12, // 0: litrpc.AddAutopilotSessionRequest.features:type_name -> litrpc.AddAutopilotSessionRequest.FeaturesEntry + 15, // 1: litrpc.AddAutopilotSessionRequest.session_rules:type_name -> litrpc.RulesMap + 15, // 2: litrpc.FeatureConfig.rules:type_name -> litrpc.RulesMap + 16, // 3: litrpc.ListAutopilotSessionsResponse.sessions:type_name -> litrpc.Session + 16, // 4: litrpc.AddAutopilotSessionResponse.session:type_name -> litrpc.Session + 13, // 5: litrpc.ListAutopilotFeaturesResponse.features:type_name -> litrpc.ListAutopilotFeaturesResponse.FeaturesEntry + 14, // 6: litrpc.Feature.rules:type_name -> litrpc.Feature.RulesEntry + 11, // 7: litrpc.Feature.permissions_list:type_name -> litrpc.Permissions + 17, // 8: litrpc.RuleValues.defaults:type_name -> litrpc.RuleValue + 17, // 9: litrpc.RuleValues.min_value:type_name -> litrpc.RuleValue + 17, // 10: litrpc.RuleValues.max_value:type_name -> litrpc.RuleValue + 18, // 11: litrpc.Permissions.operations:type_name -> litrpc.MacaroonPermission + 1, // 12: litrpc.AddAutopilotSessionRequest.FeaturesEntry.value:type_name -> litrpc.FeatureConfig + 9, // 13: litrpc.ListAutopilotFeaturesResponse.FeaturesEntry.value:type_name -> litrpc.Feature + 10, // 14: litrpc.Feature.RulesEntry.value:type_name -> litrpc.RuleValues + 5, // 15: litrpc.Autopilot.ListAutopilotFeatures:input_type -> litrpc.ListAutopilotFeaturesRequest + 0, // 16: litrpc.Autopilot.AddAutopilotSession:input_type -> litrpc.AddAutopilotSessionRequest + 2, // 17: litrpc.Autopilot.ListAutopilotSessions:input_type -> litrpc.ListAutopilotSessionsRequest + 7, // 18: litrpc.Autopilot.RevokeAutopilotSession:input_type -> litrpc.RevokeAutopilotSessionRequest + 6, // 19: litrpc.Autopilot.ListAutopilotFeatures:output_type -> litrpc.ListAutopilotFeaturesResponse + 4, // 20: litrpc.Autopilot.AddAutopilotSession:output_type -> litrpc.AddAutopilotSessionResponse + 3, // 21: litrpc.Autopilot.ListAutopilotSessions:output_type -> litrpc.ListAutopilotSessionsResponse + 8, // 22: litrpc.Autopilot.RevokeAutopilotSession:output_type -> litrpc.RevokeAutopilotSessionResponse + 19, // [19:23] is the sub-list for method output_type + 15, // [15:19] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name +} + +func init() { file_lit_autopilot_proto_init() } +func file_lit_autopilot_proto_init() { + if File_lit_autopilot_proto != nil { + return + } + file_lit_sessions_proto_init() + if !protoimpl.UnsafeEnabled { + file_lit_autopilot_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddAutopilotSessionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_autopilot_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FeatureConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_autopilot_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListAutopilotSessionsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_autopilot_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListAutopilotSessionsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_autopilot_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddAutopilotSessionResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_autopilot_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListAutopilotFeaturesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_autopilot_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListAutopilotFeaturesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_autopilot_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RevokeAutopilotSessionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_autopilot_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RevokeAutopilotSessionResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_autopilot_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Feature); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_autopilot_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RuleValues); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_autopilot_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Permissions); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_lit_autopilot_proto_rawDesc, + NumEnums: 0, + NumMessages: 15, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_lit_autopilot_proto_goTypes, + DependencyIndexes: file_lit_autopilot_proto_depIdxs, + MessageInfos: file_lit_autopilot_proto_msgTypes, + }.Build() + File_lit_autopilot_proto = out.File + file_lit_autopilot_proto_rawDesc = nil + file_lit_autopilot_proto_goTypes = nil + file_lit_autopilot_proto_depIdxs = nil +} diff --git a/litrpc/lit-autopilot.proto b/litrpc/lit-autopilot.proto new file mode 100644 index 000000000..740829d5e --- /dev/null +++ b/litrpc/lit-autopilot.proto @@ -0,0 +1,174 @@ +syntax = "proto3"; + +import "lit-sessions.proto"; + +package litrpc; + +option go_package = "github.com/lightninglabs/lightning-terminal/litrpc"; + +service Autopilot { + rpc ListAutopilotFeatures (ListAutopilotFeaturesRequest) + returns (ListAutopilotFeaturesResponse); + + rpc AddAutopilotSession (AddAutopilotSessionRequest) + returns (AddAutopilotSessionResponse); + + rpc ListAutopilotSessions (ListAutopilotSessionsRequest) + returns (ListAutopilotSessionsResponse); + + rpc RevokeAutopilotSession (RevokeAutopilotSessionRequest) + returns (RevokeAutopilotSessionResponse); +} + +message AddAutopilotSessionRequest { + /* + A human readable label to assign to the session. + */ + string label = 1; + + /* + The unix timestamp at which this session should be revoked. + */ + uint64 expiry_timestamp_seconds = 2 [jstype = JS_STRING]; + + /* + The address of the mailbox server to connect to for this session. + */ + string mailbox_server_addr = 3; + + /* + Set to true if tls should be skipped for when connecting to the mailbox. + */ + bool dev_server = 4; + + /* + The features that the session should subscribe to. Each feature maps to + a FeatureConfig that should be applied to that feature. + */ + map features = 5; + + /* + Rules that apply to the entire session. By default, no rules will apply + to the entire session. + */ + RulesMap session_rules = 6; + + /* + Set to true of the session should not make use of the privacy mapper. + */ + bool no_privacy_mapper = 7; +} + +message FeatureConfig { + /* + The RulesMap acts as an override map. In other words, by default the rules + values recommended by the Auto Pilot server will be used but the RulesMap + can be used to override the defaults. + */ + RulesMap rules = 1; + + /* + Serialised configuration for the feature. + */ + bytes config = 2; +} + +message ListAutopilotSessionsRequest { +} + +message ListAutopilotSessionsResponse { + /* + A list of the Autopilot sessions. + */ + repeated Session sessions = 1; +} + +message AddAutopilotSessionResponse { + /* + Details of the session that was just created. + */ + Session session = 1; +} + +message ListAutopilotFeaturesRequest { +} + +message ListAutopilotFeaturesResponse { + /* + A map of feature names to Feature objects describing the feature. + */ + map features = 1; +} + +message RevokeAutopilotSessionRequest { + bytes local_public_key = 1; +} + +message RevokeAutopilotSessionResponse { +} + +message Feature { + /* + Name is the name of the Autopilot feature. + */ + string name = 1; + + /* + A human readable description of what the feature offers. + */ + string description = 2; + + /* + A map of rules that make sense for this feature. Each rule is accompanied + with appropriate default values for the feature along with minimum and + maximum values for the rules. + */ + map rules = 3; + + /* + A list of URI permissions required by the feature. + */ + repeated Permissions permissions_list = 4; + + /* + A boolean indicating if the user would need to upgrade their Litd version in + order to subscribe to the Autopilot feature. This will be true if the + feature rules set contains a rule that Litd is unaware of. + */ + bool requires_upgrade = 5; +} + +message RuleValues { + /* + Whether or not the users version of Litd is aware of this rule. + */ + bool known = 1; + + /* + The default values for the rule that the Autopilot server recommends for + the associated feature. + */ + RuleValue defaults = 2; + + /* + The minimum sane value for this rule for the associated feature. + */ + RuleValue min_value = 3; + + /* + The maximum sane value for this rule for the associated feature. + */ + RuleValue max_value = 4; +} + +message Permissions { + /* + The URI in question. + */ + string method = 1; + + /* + A list of the permissions required for this method. + */ + repeated MacaroonPermission operations = 2; +} \ No newline at end of file diff --git a/litrpc/lit-autopilot_grpc.pb.go b/litrpc/lit-autopilot_grpc.pb.go new file mode 100644 index 000000000..04e53f682 --- /dev/null +++ b/litrpc/lit-autopilot_grpc.pb.go @@ -0,0 +1,209 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package litrpc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// AutopilotClient is the client API for Autopilot service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AutopilotClient interface { + ListAutopilotFeatures(ctx context.Context, in *ListAutopilotFeaturesRequest, opts ...grpc.CallOption) (*ListAutopilotFeaturesResponse, error) + AddAutopilotSession(ctx context.Context, in *AddAutopilotSessionRequest, opts ...grpc.CallOption) (*AddAutopilotSessionResponse, error) + ListAutopilotSessions(ctx context.Context, in *ListAutopilotSessionsRequest, opts ...grpc.CallOption) (*ListAutopilotSessionsResponse, error) + RevokeAutopilotSession(ctx context.Context, in *RevokeAutopilotSessionRequest, opts ...grpc.CallOption) (*RevokeAutopilotSessionResponse, error) +} + +type autopilotClient struct { + cc grpc.ClientConnInterface +} + +func NewAutopilotClient(cc grpc.ClientConnInterface) AutopilotClient { + return &autopilotClient{cc} +} + +func (c *autopilotClient) ListAutopilotFeatures(ctx context.Context, in *ListAutopilotFeaturesRequest, opts ...grpc.CallOption) (*ListAutopilotFeaturesResponse, error) { + out := new(ListAutopilotFeaturesResponse) + err := c.cc.Invoke(ctx, "/litrpc.Autopilot/ListAutopilotFeatures", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *autopilotClient) AddAutopilotSession(ctx context.Context, in *AddAutopilotSessionRequest, opts ...grpc.CallOption) (*AddAutopilotSessionResponse, error) { + out := new(AddAutopilotSessionResponse) + err := c.cc.Invoke(ctx, "/litrpc.Autopilot/AddAutopilotSession", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *autopilotClient) ListAutopilotSessions(ctx context.Context, in *ListAutopilotSessionsRequest, opts ...grpc.CallOption) (*ListAutopilotSessionsResponse, error) { + out := new(ListAutopilotSessionsResponse) + err := c.cc.Invoke(ctx, "/litrpc.Autopilot/ListAutopilotSessions", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *autopilotClient) RevokeAutopilotSession(ctx context.Context, in *RevokeAutopilotSessionRequest, opts ...grpc.CallOption) (*RevokeAutopilotSessionResponse, error) { + out := new(RevokeAutopilotSessionResponse) + err := c.cc.Invoke(ctx, "/litrpc.Autopilot/RevokeAutopilotSession", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AutopilotServer is the server API for Autopilot service. +// All implementations must embed UnimplementedAutopilotServer +// for forward compatibility +type AutopilotServer interface { + ListAutopilotFeatures(context.Context, *ListAutopilotFeaturesRequest) (*ListAutopilotFeaturesResponse, error) + AddAutopilotSession(context.Context, *AddAutopilotSessionRequest) (*AddAutopilotSessionResponse, error) + ListAutopilotSessions(context.Context, *ListAutopilotSessionsRequest) (*ListAutopilotSessionsResponse, error) + RevokeAutopilotSession(context.Context, *RevokeAutopilotSessionRequest) (*RevokeAutopilotSessionResponse, error) + mustEmbedUnimplementedAutopilotServer() +} + +// UnimplementedAutopilotServer must be embedded to have forward compatible implementations. +type UnimplementedAutopilotServer struct { +} + +func (UnimplementedAutopilotServer) ListAutopilotFeatures(context.Context, *ListAutopilotFeaturesRequest) (*ListAutopilotFeaturesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListAutopilotFeatures not implemented") +} +func (UnimplementedAutopilotServer) AddAutopilotSession(context.Context, *AddAutopilotSessionRequest) (*AddAutopilotSessionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddAutopilotSession not implemented") +} +func (UnimplementedAutopilotServer) ListAutopilotSessions(context.Context, *ListAutopilotSessionsRequest) (*ListAutopilotSessionsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListAutopilotSessions not implemented") +} +func (UnimplementedAutopilotServer) RevokeAutopilotSession(context.Context, *RevokeAutopilotSessionRequest) (*RevokeAutopilotSessionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RevokeAutopilotSession not implemented") +} +func (UnimplementedAutopilotServer) mustEmbedUnimplementedAutopilotServer() {} + +// UnsafeAutopilotServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AutopilotServer will +// result in compilation errors. +type UnsafeAutopilotServer interface { + mustEmbedUnimplementedAutopilotServer() +} + +func RegisterAutopilotServer(s grpc.ServiceRegistrar, srv AutopilotServer) { + s.RegisterService(&Autopilot_ServiceDesc, srv) +} + +func _Autopilot_ListAutopilotFeatures_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListAutopilotFeaturesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AutopilotServer).ListAutopilotFeatures(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/litrpc.Autopilot/ListAutopilotFeatures", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AutopilotServer).ListAutopilotFeatures(ctx, req.(*ListAutopilotFeaturesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Autopilot_AddAutopilotSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddAutopilotSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AutopilotServer).AddAutopilotSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/litrpc.Autopilot/AddAutopilotSession", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AutopilotServer).AddAutopilotSession(ctx, req.(*AddAutopilotSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Autopilot_ListAutopilotSessions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListAutopilotSessionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AutopilotServer).ListAutopilotSessions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/litrpc.Autopilot/ListAutopilotSessions", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AutopilotServer).ListAutopilotSessions(ctx, req.(*ListAutopilotSessionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Autopilot_RevokeAutopilotSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RevokeAutopilotSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AutopilotServer).RevokeAutopilotSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/litrpc.Autopilot/RevokeAutopilotSession", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AutopilotServer).RevokeAutopilotSession(ctx, req.(*RevokeAutopilotSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Autopilot_ServiceDesc is the grpc.ServiceDesc for Autopilot service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Autopilot_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "litrpc.Autopilot", + HandlerType: (*AutopilotServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListAutopilotFeatures", + Handler: _Autopilot_ListAutopilotFeatures_Handler, + }, + { + MethodName: "AddAutopilotSession", + Handler: _Autopilot_AddAutopilotSession_Handler, + }, + { + MethodName: "ListAutopilotSessions", + Handler: _Autopilot_ListAutopilotSessions_Handler, + }, + { + MethodName: "RevokeAutopilotSession", + Handler: _Autopilot_RevokeAutopilotSession_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "lit-autopilot.proto", +} diff --git a/litrpc/lit-sessions.pb.go b/litrpc/lit-sessions.pb.go index 6f1220818..091e35bd6 100644 --- a/litrpc/lit-sessions.pb.go +++ b/litrpc/lit-sessions.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 +// protoc-gen-go v1.28.1 // protoc v3.6.1 // source: lit-sessions.proto @@ -27,6 +27,7 @@ const ( SessionType_TYPE_MACAROON_ADMIN SessionType = 1 SessionType_TYPE_MACAROON_CUSTOM SessionType = 2 SessionType_TYPE_UI_PASSWORD SessionType = 3 + SessionType_TYPE_AUTOPILOT SessionType = 4 SessionType_TYPE_MACAROON_ACCOUNT SessionType = 5 ) @@ -37,6 +38,7 @@ var ( 1: "TYPE_MACAROON_ADMIN", 2: "TYPE_MACAROON_CUSTOM", 3: "TYPE_UI_PASSWORD", + 4: "TYPE_AUTOPILOT", 5: "TYPE_MACAROON_ACCOUNT", } SessionType_value = map[string]int32{ @@ -44,6 +46,7 @@ var ( "TYPE_MACAROON_ADMIN": 1, "TYPE_MACAROON_CUSTOM": 2, "TYPE_UI_PASSWORD": 3, + "TYPE_AUTOPILOT": 4, "TYPE_MACAROON_ACCOUNT": 5, } ) @@ -339,19 +342,21 @@ type Session struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Label string `protobuf:"bytes,1,opt,name=label,proto3" json:"label,omitempty"` - SessionState SessionState `protobuf:"varint,2,opt,name=session_state,json=sessionState,proto3,enum=litrpc.SessionState" json:"session_state,omitempty"` - SessionType SessionType `protobuf:"varint,3,opt,name=session_type,json=sessionType,proto3,enum=litrpc.SessionType" json:"session_type,omitempty"` - ExpiryTimestampSeconds uint64 `protobuf:"varint,4,opt,name=expiry_timestamp_seconds,json=expiryTimestampSeconds,proto3" json:"expiry_timestamp_seconds,omitempty"` - MailboxServerAddr string `protobuf:"bytes,5,opt,name=mailbox_server_addr,json=mailboxServerAddr,proto3" json:"mailbox_server_addr,omitempty"` - DevServer bool `protobuf:"varint,6,opt,name=dev_server,json=devServer,proto3" json:"dev_server,omitempty"` - PairingSecret []byte `protobuf:"bytes,7,opt,name=pairing_secret,json=pairingSecret,proto3" json:"pairing_secret,omitempty"` - PairingSecretMnemonic string `protobuf:"bytes,8,opt,name=pairing_secret_mnemonic,json=pairingSecretMnemonic,proto3" json:"pairing_secret_mnemonic,omitempty"` - LocalPublicKey []byte `protobuf:"bytes,9,opt,name=local_public_key,json=localPublicKey,proto3" json:"local_public_key,omitempty"` - RemotePublicKey []byte `protobuf:"bytes,10,opt,name=remote_public_key,json=remotePublicKey,proto3" json:"remote_public_key,omitempty"` - CreatedAt uint64 `protobuf:"varint,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - MacaroonRecipe *MacaroonRecipe `protobuf:"bytes,12,opt,name=macaroon_recipe,json=macaroonRecipe,proto3" json:"macaroon_recipe,omitempty"` - AccountId string `protobuf:"bytes,13,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` + Id []byte `protobuf:"bytes,14,opt,name=id,proto3" json:"id,omitempty"` + Label string `protobuf:"bytes,1,opt,name=label,proto3" json:"label,omitempty"` + SessionState SessionState `protobuf:"varint,2,opt,name=session_state,json=sessionState,proto3,enum=litrpc.SessionState" json:"session_state,omitempty"` + SessionType SessionType `protobuf:"varint,3,opt,name=session_type,json=sessionType,proto3,enum=litrpc.SessionType" json:"session_type,omitempty"` + ExpiryTimestampSeconds uint64 `protobuf:"varint,4,opt,name=expiry_timestamp_seconds,json=expiryTimestampSeconds,proto3" json:"expiry_timestamp_seconds,omitempty"` + MailboxServerAddr string `protobuf:"bytes,5,opt,name=mailbox_server_addr,json=mailboxServerAddr,proto3" json:"mailbox_server_addr,omitempty"` + DevServer bool `protobuf:"varint,6,opt,name=dev_server,json=devServer,proto3" json:"dev_server,omitempty"` + PairingSecret []byte `protobuf:"bytes,7,opt,name=pairing_secret,json=pairingSecret,proto3" json:"pairing_secret,omitempty"` + PairingSecretMnemonic string `protobuf:"bytes,8,opt,name=pairing_secret_mnemonic,json=pairingSecretMnemonic,proto3" json:"pairing_secret_mnemonic,omitempty"` + LocalPublicKey []byte `protobuf:"bytes,9,opt,name=local_public_key,json=localPublicKey,proto3" json:"local_public_key,omitempty"` + RemotePublicKey []byte `protobuf:"bytes,10,opt,name=remote_public_key,json=remotePublicKey,proto3" json:"remote_public_key,omitempty"` + CreatedAt uint64 `protobuf:"varint,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + MacaroonRecipe *MacaroonRecipe `protobuf:"bytes,12,opt,name=macaroon_recipe,json=macaroonRecipe,proto3" json:"macaroon_recipe,omitempty"` + AccountId string `protobuf:"bytes,13,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` + AutopilotFeatureInfo map[string]*RulesMap `protobuf:"bytes,15,rep,name=autopilot_feature_info,json=autopilotFeatureInfo,proto3" json:"autopilot_feature_info,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // //The unix timestamp indicating the time at which the session was revoked. //Note that this field has not been around since the beginning and so it @@ -394,6 +399,13 @@ func (*Session) Descriptor() ([]byte, []int) { return file_lit_sessions_proto_rawDescGZIP(), []int{3} } +func (x *Session) GetId() []byte { + if x != nil { + return x.Id + } + return nil +} + func (x *Session) GetLabel() string { if x != nil { return x.Label @@ -415,109 +427,853 @@ func (x *Session) GetSessionType() SessionType { return SessionType_TYPE_MACAROON_READONLY } -func (x *Session) GetExpiryTimestampSeconds() uint64 { - if x != nil { - return x.ExpiryTimestampSeconds - } - return 0 +func (x *Session) GetExpiryTimestampSeconds() uint64 { + if x != nil { + return x.ExpiryTimestampSeconds + } + return 0 +} + +func (x *Session) GetMailboxServerAddr() string { + if x != nil { + return x.MailboxServerAddr + } + return "" +} + +func (x *Session) GetDevServer() bool { + if x != nil { + return x.DevServer + } + return false +} + +func (x *Session) GetPairingSecret() []byte { + if x != nil { + return x.PairingSecret + } + return nil +} + +func (x *Session) GetPairingSecretMnemonic() string { + if x != nil { + return x.PairingSecretMnemonic + } + return "" +} + +func (x *Session) GetLocalPublicKey() []byte { + if x != nil { + return x.LocalPublicKey + } + return nil +} + +func (x *Session) GetRemotePublicKey() []byte { + if x != nil { + return x.RemotePublicKey + } + return nil +} + +func (x *Session) GetCreatedAt() uint64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *Session) GetMacaroonRecipe() *MacaroonRecipe { + if x != nil { + return x.MacaroonRecipe + } + return nil +} + +func (x *Session) GetAccountId() string { + if x != nil { + return x.AccountId + } + return "" +} + +func (x *Session) GetAutopilotFeatureInfo() map[string]*RulesMap { + if x != nil { + return x.AutopilotFeatureInfo + } + return nil +} + +func (x *Session) GetRevokedAt() uint64 { + if x != nil { + return x.RevokedAt + } + return 0 +} + +type MacaroonRecipe struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Permissions []*MacaroonPermission `protobuf:"bytes,1,rep,name=permissions,proto3" json:"permissions,omitempty"` + Caveats []string `protobuf:"bytes,2,rep,name=caveats,proto3" json:"caveats,omitempty"` +} + +func (x *MacaroonRecipe) Reset() { + *x = MacaroonRecipe{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_sessions_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MacaroonRecipe) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MacaroonRecipe) ProtoMessage() {} + +func (x *MacaroonRecipe) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MacaroonRecipe.ProtoReflect.Descriptor instead. +func (*MacaroonRecipe) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{4} +} + +func (x *MacaroonRecipe) GetPermissions() []*MacaroonPermission { + if x != nil { + return x.Permissions + } + return nil +} + +func (x *MacaroonRecipe) GetCaveats() []string { + if x != nil { + return x.Caveats + } + return nil +} + +type ListSessionsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ListSessionsRequest) Reset() { + *x = ListSessionsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_sessions_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListSessionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSessionsRequest) ProtoMessage() {} + +func (x *ListSessionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSessionsRequest.ProtoReflect.Descriptor instead. +func (*ListSessionsRequest) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{5} +} + +type ListSessionsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Sessions []*Session `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"` +} + +func (x *ListSessionsResponse) Reset() { + *x = ListSessionsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_sessions_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListSessionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSessionsResponse) ProtoMessage() {} + +func (x *ListSessionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSessionsResponse.ProtoReflect.Descriptor instead. +func (*ListSessionsResponse) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{6} +} + +func (x *ListSessionsResponse) GetSessions() []*Session { + if x != nil { + return x.Sessions + } + return nil +} + +type RevokeSessionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + LocalPublicKey []byte `protobuf:"bytes,8,opt,name=local_public_key,json=localPublicKey,proto3" json:"local_public_key,omitempty"` +} + +func (x *RevokeSessionRequest) Reset() { + *x = RevokeSessionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_sessions_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RevokeSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeSessionRequest) ProtoMessage() {} + +func (x *RevokeSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeSessionRequest.ProtoReflect.Descriptor instead. +func (*RevokeSessionRequest) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{7} +} + +func (x *RevokeSessionRequest) GetLocalPublicKey() []byte { + if x != nil { + return x.LocalPublicKey + } + return nil +} + +type RevokeSessionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *RevokeSessionResponse) Reset() { + *x = RevokeSessionResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_sessions_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RevokeSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeSessionResponse) ProtoMessage() {} + +func (x *RevokeSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeSessionResponse.ProtoReflect.Descriptor instead. +func (*RevokeSessionResponse) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{8} +} + +type RulesMap struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //A map of rule name to RuleValue. The RuleValue should be parsed based on + //the name of the rule. + Rules map[string]*RuleValue `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *RulesMap) Reset() { + *x = RulesMap{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_sessions_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RulesMap) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RulesMap) ProtoMessage() {} + +func (x *RulesMap) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RulesMap.ProtoReflect.Descriptor instead. +func (*RulesMap) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{9} +} + +func (x *RulesMap) GetRules() map[string]*RuleValue { + if x != nil { + return x.Rules + } + return nil +} + +type RuleValue struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Value: + // *RuleValue_RateLimit + // *RuleValue_ChanPolicyBounds + // *RuleValue_HistoryLimit + // *RuleValue_OffChainBudget + // *RuleValue_OnChainBudget + // *RuleValue_SendToSelf + // *RuleValue_ChannelRestrict + // *RuleValue_PeerRestrict + Value isRuleValue_Value `protobuf_oneof:"value"` +} + +func (x *RuleValue) Reset() { + *x = RuleValue{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_sessions_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RuleValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RuleValue) ProtoMessage() {} + +func (x *RuleValue) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RuleValue.ProtoReflect.Descriptor instead. +func (*RuleValue) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{10} +} + +func (m *RuleValue) GetValue() isRuleValue_Value { + if m != nil { + return m.Value + } + return nil +} + +func (x *RuleValue) GetRateLimit() *RateLimit { + if x, ok := x.GetValue().(*RuleValue_RateLimit); ok { + return x.RateLimit + } + return nil +} + +func (x *RuleValue) GetChanPolicyBounds() *ChannelPolicyBounds { + if x, ok := x.GetValue().(*RuleValue_ChanPolicyBounds); ok { + return x.ChanPolicyBounds + } + return nil +} + +func (x *RuleValue) GetHistoryLimit() *HistoryLimit { + if x, ok := x.GetValue().(*RuleValue_HistoryLimit); ok { + return x.HistoryLimit + } + return nil +} + +func (x *RuleValue) GetOffChainBudget() *OffChainBudget { + if x, ok := x.GetValue().(*RuleValue_OffChainBudget); ok { + return x.OffChainBudget + } + return nil +} + +func (x *RuleValue) GetOnChainBudget() *OnChainBudget { + if x, ok := x.GetValue().(*RuleValue_OnChainBudget); ok { + return x.OnChainBudget + } + return nil +} + +func (x *RuleValue) GetSendToSelf() *SendToSelf { + if x, ok := x.GetValue().(*RuleValue_SendToSelf); ok { + return x.SendToSelf + } + return nil +} + +func (x *RuleValue) GetChannelRestrict() *ChannelRestrict { + if x, ok := x.GetValue().(*RuleValue_ChannelRestrict); ok { + return x.ChannelRestrict + } + return nil +} + +func (x *RuleValue) GetPeerRestrict() *PeerRestrict { + if x, ok := x.GetValue().(*RuleValue_PeerRestrict); ok { + return x.PeerRestrict + } + return nil +} + +type isRuleValue_Value interface { + isRuleValue_Value() +} + +type RuleValue_RateLimit struct { + RateLimit *RateLimit `protobuf:"bytes,1,opt,name=rate_limit,json=rateLimit,proto3,oneof"` +} + +type RuleValue_ChanPolicyBounds struct { + ChanPolicyBounds *ChannelPolicyBounds `protobuf:"bytes,2,opt,name=chan_policy_bounds,json=chanPolicyBounds,proto3,oneof"` +} + +type RuleValue_HistoryLimit struct { + HistoryLimit *HistoryLimit `protobuf:"bytes,3,opt,name=history_limit,json=historyLimit,proto3,oneof"` +} + +type RuleValue_OffChainBudget struct { + OffChainBudget *OffChainBudget `protobuf:"bytes,4,opt,name=off_chain_budget,json=offChainBudget,proto3,oneof"` +} + +type RuleValue_OnChainBudget struct { + OnChainBudget *OnChainBudget `protobuf:"bytes,5,opt,name=on_chain_budget,json=onChainBudget,proto3,oneof"` +} + +type RuleValue_SendToSelf struct { + SendToSelf *SendToSelf `protobuf:"bytes,6,opt,name=send_to_self,json=sendToSelf,proto3,oneof"` +} + +type RuleValue_ChannelRestrict struct { + ChannelRestrict *ChannelRestrict `protobuf:"bytes,7,opt,name=channel_restrict,json=channelRestrict,proto3,oneof"` +} + +type RuleValue_PeerRestrict struct { + PeerRestrict *PeerRestrict `protobuf:"bytes,8,opt,name=peer_restrict,json=peerRestrict,proto3,oneof"` +} + +func (*RuleValue_RateLimit) isRuleValue_Value() {} + +func (*RuleValue_ChanPolicyBounds) isRuleValue_Value() {} + +func (*RuleValue_HistoryLimit) isRuleValue_Value() {} + +func (*RuleValue_OffChainBudget) isRuleValue_Value() {} + +func (*RuleValue_OnChainBudget) isRuleValue_Value() {} + +func (*RuleValue_SendToSelf) isRuleValue_Value() {} + +func (*RuleValue_ChannelRestrict) isRuleValue_Value() {} + +func (*RuleValue_PeerRestrict) isRuleValue_Value() {} + +type RateLimit struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The rate limit for read-only calls. + ReadLimit *Rate `protobuf:"bytes,1,opt,name=read_limit,json=readLimit,proto3" json:"read_limit,omitempty"` + // + //The rate limit for write/execution calls. + WriteLimit *Rate `protobuf:"bytes,2,opt,name=write_limit,json=writeLimit,proto3" json:"write_limit,omitempty"` +} + +func (x *RateLimit) Reset() { + *x = RateLimit{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_sessions_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RateLimit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RateLimit) ProtoMessage() {} + +func (x *RateLimit) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RateLimit.ProtoReflect.Descriptor instead. +func (*RateLimit) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{11} +} + +func (x *RateLimit) GetReadLimit() *Rate { + if x != nil { + return x.ReadLimit + } + return nil +} + +func (x *RateLimit) GetWriteLimit() *Rate { + if x != nil { + return x.WriteLimit + } + return nil +} + +type Rate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The number of times a call is allowed in num_hours number of hours. + Iterations uint32 `protobuf:"varint,1,opt,name=iterations,proto3" json:"iterations,omitempty"` + // + //The number of hours in which the iterations count takes place over. + NumHours uint32 `protobuf:"varint,2,opt,name=num_hours,json=numHours,proto3" json:"num_hours,omitempty"` +} + +func (x *Rate) Reset() { + *x = Rate{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_sessions_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Rate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Rate) ProtoMessage() {} + +func (x *Rate) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Rate.ProtoReflect.Descriptor instead. +func (*Rate) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{12} +} + +func (x *Rate) GetIterations() uint32 { + if x != nil { + return x.Iterations + } + return 0 +} + +func (x *Rate) GetNumHours() uint32 { + if x != nil { + return x.NumHours + } + return 0 +} + +type HistoryLimit struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The absolute unix timestamp in seconds before which no information should + //be shared. This should only be set if duration is not set. + StartTime uint64 `protobuf:"varint,1,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + // + //The maximum relative duration in seconds that a request is allowed to query + //for. This should only be set if start_time is not set. + Duration uint64 `protobuf:"varint,2,opt,name=duration,proto3" json:"duration,omitempty"` +} + +func (x *HistoryLimit) Reset() { + *x = HistoryLimit{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_sessions_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HistoryLimit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HistoryLimit) ProtoMessage() {} + +func (x *HistoryLimit) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HistoryLimit.ProtoReflect.Descriptor instead. +func (*HistoryLimit) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{13} +} + +func (x *HistoryLimit) GetStartTime() uint64 { + if x != nil { + return x.StartTime + } + return 0 +} + +func (x *HistoryLimit) GetDuration() uint64 { + if x != nil { + return x.Duration + } + return 0 +} + +type ChannelPolicyBounds struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The minimum base fee in msat that the autopilot can set for a channel. + MinBaseMsat uint64 `protobuf:"varint,1,opt,name=min_base_msat,json=minBaseMsat,proto3" json:"min_base_msat,omitempty"` + // + //The maximum base fee in msat that the autopilot can set for a channel. + MaxBaseMsat uint64 `protobuf:"varint,2,opt,name=max_base_msat,json=maxBaseMsat,proto3" json:"max_base_msat,omitempty"` + // + //The minimum ppm fee in msat that the autopilot can set for a channel. + MinRatePpm uint32 `protobuf:"varint,3,opt,name=min_rate_ppm,json=minRatePpm,proto3" json:"min_rate_ppm,omitempty"` + // + //The maximum ppm fee in msat that the autopilot can set for a channel. + MaxRatePpm uint32 `protobuf:"varint,4,opt,name=max_rate_ppm,json=maxRatePpm,proto3" json:"max_rate_ppm,omitempty"` + // + //The minimum cltv delta that the autopilot may set for a channel. + MinCltvDelta uint32 `protobuf:"varint,5,opt,name=min_cltv_delta,json=minCltvDelta,proto3" json:"min_cltv_delta,omitempty"` + // + //The maximum cltv delta that the autopilot may set for a channel. + MaxCltvDelta uint32 `protobuf:"varint,6,opt,name=max_cltv_delta,json=maxCltvDelta,proto3" json:"max_cltv_delta,omitempty"` + // + //The minimum htlc msat that the autopilot may set for a channel. + MinHtlcMsat uint64 `protobuf:"varint,7,opt,name=min_htlc_msat,json=minHtlcMsat,proto3" json:"min_htlc_msat,omitempty"` + // + //The maximum htlc msat that the autopilot may set for a channel. + MaxHtlcMsat uint64 `protobuf:"varint,8,opt,name=max_htlc_msat,json=maxHtlcMsat,proto3" json:"max_htlc_msat,omitempty"` +} + +func (x *ChannelPolicyBounds) Reset() { + *x = ChannelPolicyBounds{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_sessions_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChannelPolicyBounds) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *Session) GetMailboxServerAddr() string { - if x != nil { - return x.MailboxServerAddr +func (*ChannelPolicyBounds) ProtoMessage() {} + +func (x *ChannelPolicyBounds) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return "" + return mi.MessageOf(x) } -func (x *Session) GetDevServer() bool { - if x != nil { - return x.DevServer - } - return false +// Deprecated: Use ChannelPolicyBounds.ProtoReflect.Descriptor instead. +func (*ChannelPolicyBounds) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{14} } -func (x *Session) GetPairingSecret() []byte { +func (x *ChannelPolicyBounds) GetMinBaseMsat() uint64 { if x != nil { - return x.PairingSecret + return x.MinBaseMsat } - return nil + return 0 } -func (x *Session) GetPairingSecretMnemonic() string { +func (x *ChannelPolicyBounds) GetMaxBaseMsat() uint64 { if x != nil { - return x.PairingSecretMnemonic + return x.MaxBaseMsat } - return "" + return 0 } -func (x *Session) GetLocalPublicKey() []byte { +func (x *ChannelPolicyBounds) GetMinRatePpm() uint32 { if x != nil { - return x.LocalPublicKey + return x.MinRatePpm } - return nil + return 0 } -func (x *Session) GetRemotePublicKey() []byte { +func (x *ChannelPolicyBounds) GetMaxRatePpm() uint32 { if x != nil { - return x.RemotePublicKey + return x.MaxRatePpm } - return nil + return 0 } -func (x *Session) GetCreatedAt() uint64 { +func (x *ChannelPolicyBounds) GetMinCltvDelta() uint32 { if x != nil { - return x.CreatedAt + return x.MinCltvDelta } return 0 } -func (x *Session) GetMacaroonRecipe() *MacaroonRecipe { +func (x *ChannelPolicyBounds) GetMaxCltvDelta() uint32 { if x != nil { - return x.MacaroonRecipe + return x.MaxCltvDelta } - return nil + return 0 } -func (x *Session) GetAccountId() string { +func (x *ChannelPolicyBounds) GetMinHtlcMsat() uint64 { if x != nil { - return x.AccountId + return x.MinHtlcMsat } - return "" + return 0 } -func (x *Session) GetRevokedAt() uint64 { +func (x *ChannelPolicyBounds) GetMaxHtlcMsat() uint64 { if x != nil { - return x.RevokedAt + return x.MaxHtlcMsat } return 0 } -type MacaroonRecipe struct { +type OffChainBudget struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Permissions []*MacaroonPermission `protobuf:"bytes,1,rep,name=permissions,proto3" json:"permissions,omitempty"` - Caveats []string `protobuf:"bytes,2,rep,name=caveats,proto3" json:"caveats,omitempty"` + MaxAmtMsat uint64 `protobuf:"varint,1,opt,name=max_amt_msat,json=maxAmtMsat,proto3" json:"max_amt_msat,omitempty"` + MaxFeesMsat uint64 `protobuf:"varint,2,opt,name=max_fees_msat,json=maxFeesMsat,proto3" json:"max_fees_msat,omitempty"` } -func (x *MacaroonRecipe) Reset() { - *x = MacaroonRecipe{} +func (x *OffChainBudget) Reset() { + *x = OffChainBudget{} if protoimpl.UnsafeEnabled { - mi := &file_lit_sessions_proto_msgTypes[4] + mi := &file_lit_sessions_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *MacaroonRecipe) String() string { +func (x *OffChainBudget) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MacaroonRecipe) ProtoMessage() {} +func (*OffChainBudget) ProtoMessage() {} -func (x *MacaroonRecipe) ProtoReflect() protoreflect.Message { - mi := &file_lit_sessions_proto_msgTypes[4] +func (x *OffChainBudget) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -528,48 +1284,51 @@ func (x *MacaroonRecipe) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MacaroonRecipe.ProtoReflect.Descriptor instead. -func (*MacaroonRecipe) Descriptor() ([]byte, []int) { - return file_lit_sessions_proto_rawDescGZIP(), []int{4} +// Deprecated: Use OffChainBudget.ProtoReflect.Descriptor instead. +func (*OffChainBudget) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{15} } -func (x *MacaroonRecipe) GetPermissions() []*MacaroonPermission { +func (x *OffChainBudget) GetMaxAmtMsat() uint64 { if x != nil { - return x.Permissions + return x.MaxAmtMsat } - return nil + return 0 } -func (x *MacaroonRecipe) GetCaveats() []string { +func (x *OffChainBudget) GetMaxFeesMsat() uint64 { if x != nil { - return x.Caveats + return x.MaxFeesMsat } - return nil + return 0 } -type ListSessionsRequest struct { +type OnChainBudget struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + + AbsoluteAmtSats uint64 `protobuf:"varint,1,opt,name=absolute_amt_sats,json=absoluteAmtSats,proto3" json:"absolute_amt_sats,omitempty"` + MaxSatPerVByte uint64 `protobuf:"varint,2,opt,name=max_sat_per_v_byte,json=maxSatPerVByte,proto3" json:"max_sat_per_v_byte,omitempty"` } -func (x *ListSessionsRequest) Reset() { - *x = ListSessionsRequest{} +func (x *OnChainBudget) Reset() { + *x = OnChainBudget{} if protoimpl.UnsafeEnabled { - mi := &file_lit_sessions_proto_msgTypes[5] + mi := &file_lit_sessions_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *ListSessionsRequest) String() string { +func (x *OnChainBudget) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListSessionsRequest) ProtoMessage() {} +func (*OnChainBudget) ProtoMessage() {} -func (x *ListSessionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_lit_sessions_proto_msgTypes[5] +func (x *OnChainBudget) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -580,36 +1339,48 @@ func (x *ListSessionsRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListSessionsRequest.ProtoReflect.Descriptor instead. -func (*ListSessionsRequest) Descriptor() ([]byte, []int) { - return file_lit_sessions_proto_rawDescGZIP(), []int{5} +// Deprecated: Use OnChainBudget.ProtoReflect.Descriptor instead. +func (*OnChainBudget) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{16} } -type ListSessionsResponse struct { +func (x *OnChainBudget) GetAbsoluteAmtSats() uint64 { + if x != nil { + return x.AbsoluteAmtSats + } + return 0 +} + +func (x *OnChainBudget) GetMaxSatPerVByte() uint64 { + if x != nil { + return x.MaxSatPerVByte + } + return 0 +} + +type SendToSelf struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - - Sessions []*Session `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"` } -func (x *ListSessionsResponse) Reset() { - *x = ListSessionsResponse{} +func (x *SendToSelf) Reset() { + *x = SendToSelf{} if protoimpl.UnsafeEnabled { - mi := &file_lit_sessions_proto_msgTypes[6] + mi := &file_lit_sessions_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *ListSessionsResponse) String() string { +func (x *SendToSelf) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListSessionsResponse) ProtoMessage() {} +func (*SendToSelf) ProtoMessage() {} -func (x *ListSessionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_lit_sessions_proto_msgTypes[6] +func (x *SendToSelf) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -620,43 +1391,36 @@ func (x *ListSessionsResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListSessionsResponse.ProtoReflect.Descriptor instead. -func (*ListSessionsResponse) Descriptor() ([]byte, []int) { - return file_lit_sessions_proto_rawDescGZIP(), []int{6} -} - -func (x *ListSessionsResponse) GetSessions() []*Session { - if x != nil { - return x.Sessions - } - return nil +// Deprecated: Use SendToSelf.ProtoReflect.Descriptor instead. +func (*SendToSelf) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{17} } -type RevokeSessionRequest struct { +type ChannelRestrict struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - LocalPublicKey []byte `protobuf:"bytes,8,opt,name=local_public_key,json=localPublicKey,proto3" json:"local_public_key,omitempty"` + ChannelIds []uint64 `protobuf:"varint,1,rep,packed,name=channel_ids,json=channelIds,proto3" json:"channel_ids,omitempty"` } -func (x *RevokeSessionRequest) Reset() { - *x = RevokeSessionRequest{} +func (x *ChannelRestrict) Reset() { + *x = ChannelRestrict{} if protoimpl.UnsafeEnabled { - mi := &file_lit_sessions_proto_msgTypes[7] + mi := &file_lit_sessions_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *RevokeSessionRequest) String() string { +func (x *ChannelRestrict) String() string { return protoimpl.X.MessageStringOf(x) } -func (*RevokeSessionRequest) ProtoMessage() {} +func (*ChannelRestrict) ProtoMessage() {} -func (x *RevokeSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_lit_sessions_proto_msgTypes[7] +func (x *ChannelRestrict) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -667,41 +1431,43 @@ func (x *RevokeSessionRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use RevokeSessionRequest.ProtoReflect.Descriptor instead. -func (*RevokeSessionRequest) Descriptor() ([]byte, []int) { - return file_lit_sessions_proto_rawDescGZIP(), []int{7} +// Deprecated: Use ChannelRestrict.ProtoReflect.Descriptor instead. +func (*ChannelRestrict) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{18} } -func (x *RevokeSessionRequest) GetLocalPublicKey() []byte { +func (x *ChannelRestrict) GetChannelIds() []uint64 { if x != nil { - return x.LocalPublicKey + return x.ChannelIds } return nil } -type RevokeSessionResponse struct { +type PeerRestrict struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + + PeerIds []string `protobuf:"bytes,1,rep,name=peer_ids,json=peerIds,proto3" json:"peer_ids,omitempty"` } -func (x *RevokeSessionResponse) Reset() { - *x = RevokeSessionResponse{} +func (x *PeerRestrict) Reset() { + *x = PeerRestrict{} if protoimpl.UnsafeEnabled { - mi := &file_lit_sessions_proto_msgTypes[8] + mi := &file_lit_sessions_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *RevokeSessionResponse) String() string { +func (x *PeerRestrict) String() string { return protoimpl.X.MessageStringOf(x) } -func (*RevokeSessionResponse) ProtoMessage() {} +func (*PeerRestrict) ProtoMessage() {} -func (x *RevokeSessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_lit_sessions_proto_msgTypes[8] +func (x *PeerRestrict) ProtoReflect() protoreflect.Message { + mi := &file_lit_sessions_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -712,9 +1478,16 @@ func (x *RevokeSessionResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use RevokeSessionResponse.ProtoReflect.Descriptor instead. -func (*RevokeSessionResponse) Descriptor() ([]byte, []int) { - return file_lit_sessions_proto_rawDescGZIP(), []int{8} +// Deprecated: Use PeerRestrict.ProtoReflect.Descriptor instead. +func (*PeerRestrict) Descriptor() ([]byte, []int) { + return file_lit_sessions_proto_rawDescGZIP(), []int{19} +} + +func (x *PeerRestrict) GetPeerIds() []string { + if x != nil { + return x.PeerIds + } + return nil } var File_lit_sessions_proto protoreflect.FileDescriptor @@ -753,7 +1526,8 @@ var file_lit_sessions_proto_rawDesc = []byte{ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, - 0x86, 0x05, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, + 0xc6, 0x06, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x39, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, @@ -790,10 +1564,21 @@ var file_lit_sessions_proto_rawDesc = []byte{ 0x72, 0x6f, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x69, 0x70, 0x65, 0x52, 0x0e, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x69, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0a, 0x72, 0x65, 0x76, - 0x6f, 0x6b, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, - 0x01, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x4a, 0x04, 0x08, 0x0e, - 0x10, 0x0f, 0x4a, 0x04, 0x08, 0x0f, 0x10, 0x10, 0x22, 0x68, 0x0a, 0x0e, 0x4d, 0x61, 0x63, 0x61, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x5f, 0x0a, 0x16, 0x61, 0x75, 0x74, + 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x5f, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x69, + 0x6e, 0x66, 0x6f, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6c, 0x69, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x70, + 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, + 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x21, 0x0a, 0x0a, 0x72, 0x65, + 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, + 0x30, 0x01, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x59, 0x0a, + 0x19, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6c, 0x69, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x68, 0x0a, 0x0e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x69, 0x70, 0x65, 0x12, 0x3c, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, @@ -811,41 +1596,140 @@ var file_lit_sessions_proto_rawDesc = []byte{ 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x17, 0x0a, 0x15, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x93, 0x01, 0x0a, 0x0b, 0x53, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x16, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x4f, - 0x4e, 0x4c, 0x59, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, - 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, 0x5f, 0x41, 0x44, 0x4d, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x18, - 0x0a, 0x14, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, 0x5f, - 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, - 0x5f, 0x55, 0x49, 0x5f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x03, 0x12, 0x19, - 0x0a, 0x15, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, 0x5f, - 0x41, 0x43, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x10, 0x05, 0x22, 0x04, 0x08, 0x04, 0x10, 0x04, 0x2a, - 0x59, 0x0a, 0x0c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, - 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x5f, 0x55, - 0x53, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x52, 0x45, - 0x56, 0x4f, 0x4b, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, - 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, 0x03, 0x32, 0xe8, 0x01, 0x0a, 0x08, 0x53, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x43, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x53, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, - 0x64, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1a, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0c, - 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x2e, 0x6c, - 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x69, 0x74, 0x72, - 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0d, 0x52, 0x65, 0x76, 0x6f, 0x6b, - 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, - 0x63, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, - 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, - 0x73, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x65, 0x72, 0x6d, - 0x69, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8a, 0x01, 0x0a, 0x08, 0x52, 0x75, + 0x6c, 0x65, 0x73, 0x4d, 0x61, 0x70, 0x12, 0x31, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, + 0x75, 0x6c, 0x65, 0x73, 0x4d, 0x61, 0x70, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x1a, 0x4b, 0x0a, 0x0a, 0x52, 0x75, 0x6c, + 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x27, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x92, 0x04, 0x0a, 0x09, 0x52, 0x75, 0x6c, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x32, 0x0a, 0x0a, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x6c, 0x69, 0x6d, + 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x48, 0x00, 0x52, 0x09, 0x72, + 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x4b, 0x0a, 0x12, 0x63, 0x68, 0x61, 0x6e, + 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, + 0x73, 0x48, 0x00, 0x52, 0x10, 0x63, 0x68, 0x61, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, + 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, 0x3b, 0x0a, 0x0d, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, + 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, + 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x12, 0x42, 0x0a, 0x10, 0x6f, 0x66, 0x66, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, + 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, + 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x66, 0x66, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x42, 0x75, + 0x64, 0x67, 0x65, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x6f, 0x66, 0x66, 0x43, 0x68, 0x61, 0x69, 0x6e, + 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x12, 0x3f, 0x0a, 0x0f, 0x6f, 0x6e, 0x5f, 0x63, 0x68, 0x61, + 0x69, 0x6e, 0x5f, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, + 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x69, + 0x6e, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x12, 0x36, 0x0a, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x5f, + 0x74, 0x6f, 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x53, 0x65, 0x6c, + 0x66, 0x48, 0x00, 0x52, 0x0a, 0x73, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x53, 0x65, 0x6c, 0x66, 0x12, + 0x44, 0x0a, 0x10, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x72, + 0x69, 0x63, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x69, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, + 0x63, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, + 0x74, 0x72, 0x69, 0x63, 0x74, 0x12, 0x3b, 0x0a, 0x0d, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x72, 0x65, + 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, + 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, + 0x63, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, + 0x63, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x67, 0x0a, 0x09, 0x52, + 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2b, 0x0a, 0x0a, 0x72, 0x65, 0x61, 0x64, + 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6c, + 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x52, 0x09, 0x72, 0x65, 0x61, 0x64, + 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2d, 0x0a, 0x0b, 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x6c, + 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6c, 0x69, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x77, 0x72, 0x69, 0x74, 0x65, 0x4c, + 0x69, 0x6d, 0x69, 0x74, 0x22, 0x43, 0x0a, 0x04, 0x52, 0x61, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, + 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x0a, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x0a, 0x09, + 0x6e, 0x75, 0x6d, 0x5f, 0x68, 0x6f, 0x75, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x08, 0x6e, 0x75, 0x6d, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x22, 0x51, 0x0a, 0x0c, 0x48, 0x69, 0x73, + 0x74, 0x6f, 0x72, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x21, 0x0a, 0x0a, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, + 0x01, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x08, + 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, + 0x30, 0x01, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xc5, 0x02, 0x0a, + 0x13, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x6f, + 0x75, 0x6e, 0x64, 0x73, 0x12, 0x26, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x61, 0x73, 0x65, + 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, + 0x0b, 0x6d, 0x69, 0x6e, 0x42, 0x61, 0x73, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x26, 0x0a, 0x0d, + 0x6d, 0x61, 0x78, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x42, 0x61, 0x73, 0x65, + 0x4d, 0x73, 0x61, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x6d, 0x69, 0x6e, 0x5f, 0x72, 0x61, 0x74, 0x65, + 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6d, 0x69, 0x6e, 0x52, + 0x61, 0x74, 0x65, 0x50, 0x70, 0x6d, 0x12, 0x20, 0x0a, 0x0c, 0x6d, 0x61, 0x78, 0x5f, 0x72, 0x61, + 0x74, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6d, 0x61, + 0x78, 0x52, 0x61, 0x74, 0x65, 0x50, 0x70, 0x6d, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x69, 0x6e, 0x5f, + 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x0c, 0x6d, 0x69, 0x6e, 0x43, 0x6c, 0x74, 0x76, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x24, + 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x43, 0x6c, 0x74, 0x76, 0x44, + 0x65, 0x6c, 0x74, 0x61, 0x12, 0x26, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x5f, 0x68, 0x74, 0x6c, 0x63, + 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, + 0x0b, 0x6d, 0x69, 0x6e, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x26, 0x0a, 0x0d, + 0x6d, 0x61, 0x78, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x48, 0x74, 0x6c, 0x63, + 0x4d, 0x73, 0x61, 0x74, 0x22, 0x5e, 0x0a, 0x0e, 0x4f, 0x66, 0x66, 0x43, 0x68, 0x61, 0x69, 0x6e, + 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x12, 0x24, 0x0a, 0x0c, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x6d, + 0x74, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, + 0x52, 0x0a, 0x6d, 0x61, 0x78, 0x41, 0x6d, 0x74, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x26, 0x0a, 0x0d, + 0x6d, 0x61, 0x78, 0x5f, 0x66, 0x65, 0x65, 0x73, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x46, 0x65, 0x65, 0x73, + 0x4d, 0x73, 0x61, 0x74, 0x22, 0x6f, 0x0a, 0x0d, 0x4f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x42, + 0x75, 0x64, 0x67, 0x65, 0x74, 0x12, 0x2e, 0x0a, 0x11, 0x61, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, + 0x65, 0x5f, 0x61, 0x6d, 0x74, 0x5f, 0x73, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, + 0x42, 0x02, 0x30, 0x01, 0x52, 0x0f, 0x61, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x41, 0x6d, + 0x74, 0x53, 0x61, 0x74, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x61, 0x74, + 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x76, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, + 0x56, 0x42, 0x79, 0x74, 0x65, 0x22, 0x0c, 0x0a, 0x0a, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x53, + 0x65, 0x6c, 0x66, 0x22, 0x36, 0x0a, 0x0f, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, + 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x12, 0x23, 0x0a, 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, + 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x73, 0x22, 0x29, 0x0a, 0x0c, 0x50, + 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x70, + 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x70, + 0x65, 0x65, 0x72, 0x49, 0x64, 0x73, 0x2a, 0xa1, 0x01, 0x0a, 0x0b, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x16, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, + 0x41, 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x4f, 0x4e, 0x4c, 0x59, + 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x41, 0x52, + 0x4f, 0x4f, 0x4e, 0x5f, 0x41, 0x44, 0x4d, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x54, + 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, 0x5f, 0x43, 0x55, 0x53, + 0x54, 0x4f, 0x4d, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x49, + 0x5f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x54, + 0x59, 0x50, 0x45, 0x5f, 0x41, 0x55, 0x54, 0x4f, 0x50, 0x49, 0x4c, 0x4f, 0x54, 0x10, 0x04, 0x12, + 0x19, 0x0a, 0x15, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, + 0x5f, 0x41, 0x43, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x10, 0x05, 0x2a, 0x59, 0x0a, 0x0c, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, + 0x41, 0x54, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, + 0x0c, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x5f, 0x55, 0x53, 0x45, 0x10, 0x01, 0x12, + 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x44, + 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x45, 0x58, 0x50, 0x49, + 0x52, 0x45, 0x44, 0x10, 0x03, 0x32, 0xe8, 0x01, 0x0a, 0x08, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x43, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x19, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x69, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0d, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, + 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, + 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, + 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x69, 0x67, + 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x2f, + 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -861,7 +1745,7 @@ func file_lit_sessions_proto_rawDescGZIP() []byte { } var file_lit_sessions_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_lit_sessions_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_lit_sessions_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_lit_sessions_proto_goTypes = []interface{}{ (SessionType)(0), // 0: litrpc.SessionType (SessionState)(0), // 1: litrpc.SessionState @@ -874,6 +1758,19 @@ var file_lit_sessions_proto_goTypes = []interface{}{ (*ListSessionsResponse)(nil), // 8: litrpc.ListSessionsResponse (*RevokeSessionRequest)(nil), // 9: litrpc.RevokeSessionRequest (*RevokeSessionResponse)(nil), // 10: litrpc.RevokeSessionResponse + (*RulesMap)(nil), // 11: litrpc.RulesMap + (*RuleValue)(nil), // 12: litrpc.RuleValue + (*RateLimit)(nil), // 13: litrpc.RateLimit + (*Rate)(nil), // 14: litrpc.Rate + (*HistoryLimit)(nil), // 15: litrpc.HistoryLimit + (*ChannelPolicyBounds)(nil), // 16: litrpc.ChannelPolicyBounds + (*OffChainBudget)(nil), // 17: litrpc.OffChainBudget + (*OnChainBudget)(nil), // 18: litrpc.OnChainBudget + (*SendToSelf)(nil), // 19: litrpc.SendToSelf + (*ChannelRestrict)(nil), // 20: litrpc.ChannelRestrict + (*PeerRestrict)(nil), // 21: litrpc.PeerRestrict + nil, // 22: litrpc.Session.AutopilotFeatureInfoEntry + nil, // 23: litrpc.RulesMap.RulesEntry } var file_lit_sessions_proto_depIdxs = []int32{ 0, // 0: litrpc.AddSessionRequest.session_type:type_name -> litrpc.SessionType @@ -882,19 +1779,33 @@ var file_lit_sessions_proto_depIdxs = []int32{ 1, // 3: litrpc.Session.session_state:type_name -> litrpc.SessionState 0, // 4: litrpc.Session.session_type:type_name -> litrpc.SessionType 6, // 5: litrpc.Session.macaroon_recipe:type_name -> litrpc.MacaroonRecipe - 3, // 6: litrpc.MacaroonRecipe.permissions:type_name -> litrpc.MacaroonPermission - 5, // 7: litrpc.ListSessionsResponse.sessions:type_name -> litrpc.Session - 2, // 8: litrpc.Sessions.AddSession:input_type -> litrpc.AddSessionRequest - 7, // 9: litrpc.Sessions.ListSessions:input_type -> litrpc.ListSessionsRequest - 9, // 10: litrpc.Sessions.RevokeSession:input_type -> litrpc.RevokeSessionRequest - 4, // 11: litrpc.Sessions.AddSession:output_type -> litrpc.AddSessionResponse - 8, // 12: litrpc.Sessions.ListSessions:output_type -> litrpc.ListSessionsResponse - 10, // 13: litrpc.Sessions.RevokeSession:output_type -> litrpc.RevokeSessionResponse - 11, // [11:14] is the sub-list for method output_type - 8, // [8:11] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 22, // 6: litrpc.Session.autopilot_feature_info:type_name -> litrpc.Session.AutopilotFeatureInfoEntry + 3, // 7: litrpc.MacaroonRecipe.permissions:type_name -> litrpc.MacaroonPermission + 5, // 8: litrpc.ListSessionsResponse.sessions:type_name -> litrpc.Session + 23, // 9: litrpc.RulesMap.rules:type_name -> litrpc.RulesMap.RulesEntry + 13, // 10: litrpc.RuleValue.rate_limit:type_name -> litrpc.RateLimit + 16, // 11: litrpc.RuleValue.chan_policy_bounds:type_name -> litrpc.ChannelPolicyBounds + 15, // 12: litrpc.RuleValue.history_limit:type_name -> litrpc.HistoryLimit + 17, // 13: litrpc.RuleValue.off_chain_budget:type_name -> litrpc.OffChainBudget + 18, // 14: litrpc.RuleValue.on_chain_budget:type_name -> litrpc.OnChainBudget + 19, // 15: litrpc.RuleValue.send_to_self:type_name -> litrpc.SendToSelf + 20, // 16: litrpc.RuleValue.channel_restrict:type_name -> litrpc.ChannelRestrict + 21, // 17: litrpc.RuleValue.peer_restrict:type_name -> litrpc.PeerRestrict + 14, // 18: litrpc.RateLimit.read_limit:type_name -> litrpc.Rate + 14, // 19: litrpc.RateLimit.write_limit:type_name -> litrpc.Rate + 11, // 20: litrpc.Session.AutopilotFeatureInfoEntry.value:type_name -> litrpc.RulesMap + 12, // 21: litrpc.RulesMap.RulesEntry.value:type_name -> litrpc.RuleValue + 2, // 22: litrpc.Sessions.AddSession:input_type -> litrpc.AddSessionRequest + 7, // 23: litrpc.Sessions.ListSessions:input_type -> litrpc.ListSessionsRequest + 9, // 24: litrpc.Sessions.RevokeSession:input_type -> litrpc.RevokeSessionRequest + 4, // 25: litrpc.Sessions.AddSession:output_type -> litrpc.AddSessionResponse + 8, // 26: litrpc.Sessions.ListSessions:output_type -> litrpc.ListSessionsResponse + 10, // 27: litrpc.Sessions.RevokeSession:output_type -> litrpc.RevokeSessionResponse + 25, // [25:28] is the sub-list for method output_type + 22, // [22:25] is the sub-list for method input_type + 22, // [22:22] is the sub-list for extension type_name + 22, // [22:22] is the sub-list for extension extendee + 0, // [0:22] is the sub-list for field type_name } func init() { file_lit_sessions_proto_init() } @@ -1011,6 +1922,148 @@ func file_lit_sessions_proto_init() { return nil } } + file_lit_sessions_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RulesMap); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_sessions_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RuleValue); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_sessions_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RateLimit); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_sessions_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Rate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_sessions_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HistoryLimit); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_sessions_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChannelPolicyBounds); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_sessions_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OffChainBudget); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_sessions_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OnChainBudget); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_sessions_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SendToSelf); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_sessions_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChannelRestrict); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_sessions_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PeerRestrict); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_lit_sessions_proto_msgTypes[10].OneofWrappers = []interface{}{ + (*RuleValue_RateLimit)(nil), + (*RuleValue_ChanPolicyBounds)(nil), + (*RuleValue_HistoryLimit)(nil), + (*RuleValue_OffChainBudget)(nil), + (*RuleValue_OnChainBudget)(nil), + (*RuleValue_SendToSelf)(nil), + (*RuleValue_ChannelRestrict)(nil), + (*RuleValue_PeerRestrict)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -1018,7 +2071,7 @@ func file_lit_sessions_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_lit_sessions_proto_rawDesc, NumEnums: 2, - NumMessages: 9, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/litrpc/lit-sessions.proto b/litrpc/lit-sessions.proto index b47897a08..27737f332 100644 --- a/litrpc/lit-sessions.proto +++ b/litrpc/lit-sessions.proto @@ -19,7 +19,7 @@ enum SessionType { TYPE_MACAROON_ADMIN = 1; TYPE_MACAROON_CUSTOM = 2; TYPE_UI_PASSWORD = 3; - reserved 4; + TYPE_AUTOPILOT = 4; TYPE_MACAROON_ACCOUNT = 5; } @@ -67,6 +67,8 @@ message AddSessionResponse { } message Session { + bytes id = 14; + string label = 1; SessionState session_state = 2; @@ -93,15 +95,7 @@ message Session { string account_id = 13; - /* - Field number 14 is reserved for future use. - */ - reserved 14; - - /* - Field number 15 is reserved for future use. - */ - reserved 15; + map autopilot_feature_info = 15; /* The unix timestamp indicating the time at which the session was revoked. @@ -133,3 +127,125 @@ message RevokeSessionRequest { message RevokeSessionResponse { } + +message RulesMap { + /* + A map of rule name to RuleValue. The RuleValue should be parsed based on + the name of the rule. + */ + map rules = 1; +} + +message RuleValue { + oneof value { + RateLimit rate_limit = 1; + ChannelPolicyBounds chan_policy_bounds = 2; + HistoryLimit history_limit = 3; + OffChainBudget off_chain_budget = 4; + OnChainBudget on_chain_budget = 5; + SendToSelf send_to_self = 6; + ChannelRestrict channel_restrict = 7; + PeerRestrict peer_restrict = 8; + } +} + +message RateLimit { + /* + The rate limit for read-only calls. + */ + Rate read_limit = 1; + + /* + The rate limit for write/execution calls. + */ + Rate write_limit = 2; +} + +message Rate { + /* + The number of times a call is allowed in num_hours number of hours. + */ + uint32 iterations = 1; + + /* + The number of hours in which the iterations count takes place over. + */ + uint32 num_hours = 2; +} + +message HistoryLimit { + /* + The absolute unix timestamp in seconds before which no information should + be shared. This should only be set if duration is not set. + */ + uint64 start_time = 1 [jstype = JS_STRING]; + + /* + The maximum relative duration in seconds that a request is allowed to query + for. This should only be set if start_time is not set. + */ + uint64 duration = 2 [jstype = JS_STRING]; +} + +message ChannelPolicyBounds { + /* + The minimum base fee in msat that the autopilot can set for a channel. + */ + uint64 min_base_msat = 1 [jstype = JS_STRING]; + + /* + The maximum base fee in msat that the autopilot can set for a channel. + */ + uint64 max_base_msat = 2 [jstype = JS_STRING]; + + /* + The minimum ppm fee in msat that the autopilot can set for a channel. + */ + uint32 min_rate_ppm = 3; + + /* + The maximum ppm fee in msat that the autopilot can set for a channel. + */ + uint32 max_rate_ppm = 4; + + /* + The minimum cltv delta that the autopilot may set for a channel. + */ + uint32 min_cltv_delta = 5; + + /* + The maximum cltv delta that the autopilot may set for a channel. + */ + uint32 max_cltv_delta = 6; + + /* + The minimum htlc msat that the autopilot may set for a channel. + */ + uint64 min_htlc_msat = 7 [jstype = JS_STRING]; + + /* + The maximum htlc msat that the autopilot may set for a channel. + */ + uint64 max_htlc_msat = 8 [jstype = JS_STRING]; +} + +message OffChainBudget { + uint64 max_amt_msat = 1 [jstype = JS_STRING]; + uint64 max_fees_msat = 2 [jstype = JS_STRING]; +} + +message OnChainBudget { + uint64 absolute_amt_sats = 1 [jstype = JS_STRING]; + uint64 max_sat_per_v_byte = 2 [jstype = JS_STRING]; +} + +message SendToSelf { +} + +message ChannelRestrict { + repeated uint64 channel_ids = 1 [jstype = JS_STRING]; +} + +message PeerRestrict { + repeated string peer_ids = 1; +} \ No newline at end of file diff --git a/litrpc/sessions.pb.json.go b/litrpc/sessions.pb.json.go new file mode 100644 index 000000000..c49bd96d6 --- /dev/null +++ b/litrpc/sessions.pb.json.go @@ -0,0 +1,98 @@ +// Code generated by falafel 0.9.1. DO NOT EDIT. +// source: lit-sessions.proto + +package litrpc + +import ( + "context" + + gateway "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protojson" +) + +func RegisterSessionsJSONCallbacks(registry map[string]func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error))) { + + marshaler := &gateway.JSONPb{ + MarshalOptions: protojson.MarshalOptions{ + UseProtoNames: true, + EmitUnpopulated: true, + }, + } + + registry["litrpc.Sessions.AddSession"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &AddSessionRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewSessionsClient(conn) + resp, err := client.AddSession(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + + registry["litrpc.Sessions.ListSessions"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &ListSessionsRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewSessionsClient(conn) + resp, err := client.ListSessions(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + + registry["litrpc.Sessions.RevokeSession"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &RevokeSessionRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewSessionsClient(conn) + resp, err := client.RevokeSession(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } +} diff --git a/log.go b/log.go index 424b60601..96457a66f 100644 --- a/log.go +++ b/log.go @@ -5,7 +5,10 @@ import ( "github.com/lightninglabs/faraday" "github.com/lightninglabs/lightning-node-connect/mailbox" "github.com/lightninglabs/lightning-terminal/accounts" + "github.com/lightninglabs/lightning-terminal/autopilotserver" + "github.com/lightninglabs/lightning-terminal/firewall" mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" + "github.com/lightninglabs/lightning-terminal/rules" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightninglabs/loop/loopd" "github.com/lightninglabs/pool" @@ -65,6 +68,14 @@ func SetupLoggers(root *build.RotatingLogWriter, intercept signal.Interceptor) { lnd.AddSubLogger( root, accounts.Subsystem, intercept, accounts.UseLogger, ) + lnd.AddSubLogger( + root, firewall.Subsystem, intercept, firewall.UseLogger, + ) + lnd.AddSubLogger(root, rules.Subsystem, intercept, rules.UseLogger) + lnd.AddSubLogger( + root, autopilotserver.Subsystem, intercept, + autopilotserver.UseLogger, + ) // Add daemon loggers to lnd's root logger. faraday.SetupLoggers(root, intercept) diff --git a/perms/permissions.go b/perms/permissions.go index cfc113551..9fb8727f4 100644 --- a/perms/permissions.go +++ b/perms/permissions.go @@ -62,6 +62,30 @@ var ( Entity: "account", Action: "write", }}, + "/litrpc.Firewall/ListActions": {{ + Entity: "actions", + Action: "read", + }}, + "/litrpc.Autopilot/ListAutopilotFeatures": {{ + Entity: "autopilot", + Action: "read", + }}, + "/litrpc.Autopilot/AddAutopilotSession": {{ + Entity: "autopilot", + Action: "write", + }}, + "/litrpc.Autopilot/ListAutopilotSessions": {{ + Entity: "autopilot", + Action: "read", + }}, + "/litrpc.Autopilot/RevokeAutopilotSession": {{ + Entity: "autopilot", + Action: "write", + }}, + "/litrpc.Firewall/PrivacyMapConversion": {{ + Entity: "privacymap", + Action: "read", + }}, } // whiteListedLNDMethods is a map of all lnd RPC methods that don't diff --git a/proto/lit-sessions.proto b/proto/lit-sessions.proto index 9661c0cf2..27737f332 100644 --- a/proto/lit-sessions.proto +++ b/proto/lit-sessions.proto @@ -19,7 +19,7 @@ enum SessionType { TYPE_MACAROON_ADMIN = 1; TYPE_MACAROON_CUSTOM = 2; TYPE_UI_PASSWORD = 3; - reserved 4; + TYPE_AUTOPILOT = 4; TYPE_MACAROON_ACCOUNT = 5; } @@ -67,6 +67,8 @@ message AddSessionResponse { } message Session { + bytes id = 14; + string label = 1; SessionState session_state = 2; @@ -92,6 +94,18 @@ message Session { MacaroonRecipe macaroon_recipe = 12; string account_id = 13; + + map autopilot_feature_info = 15; + + /* + The unix timestamp indicating the time at which the session was revoked. + Note that this field has not been around since the beginning and so it + could be the case that a session has been revoked but that this field + will not have been set for that session. Therefore, it is suggested that + readers should not assume that if this field is zero that the session is + not revoked. Readers should instead first check the session_state field. + */ + uint64 revoked_at = 16 [jstype = JS_STRING]; } message MacaroonRecipe { @@ -113,3 +127,125 @@ message RevokeSessionRequest { message RevokeSessionResponse { } + +message RulesMap { + /* + A map of rule name to RuleValue. The RuleValue should be parsed based on + the name of the rule. + */ + map rules = 1; +} + +message RuleValue { + oneof value { + RateLimit rate_limit = 1; + ChannelPolicyBounds chan_policy_bounds = 2; + HistoryLimit history_limit = 3; + OffChainBudget off_chain_budget = 4; + OnChainBudget on_chain_budget = 5; + SendToSelf send_to_self = 6; + ChannelRestrict channel_restrict = 7; + PeerRestrict peer_restrict = 8; + } +} + +message RateLimit { + /* + The rate limit for read-only calls. + */ + Rate read_limit = 1; + + /* + The rate limit for write/execution calls. + */ + Rate write_limit = 2; +} + +message Rate { + /* + The number of times a call is allowed in num_hours number of hours. + */ + uint32 iterations = 1; + + /* + The number of hours in which the iterations count takes place over. + */ + uint32 num_hours = 2; +} + +message HistoryLimit { + /* + The absolute unix timestamp in seconds before which no information should + be shared. This should only be set if duration is not set. + */ + uint64 start_time = 1 [jstype = JS_STRING]; + + /* + The maximum relative duration in seconds that a request is allowed to query + for. This should only be set if start_time is not set. + */ + uint64 duration = 2 [jstype = JS_STRING]; +} + +message ChannelPolicyBounds { + /* + The minimum base fee in msat that the autopilot can set for a channel. + */ + uint64 min_base_msat = 1 [jstype = JS_STRING]; + + /* + The maximum base fee in msat that the autopilot can set for a channel. + */ + uint64 max_base_msat = 2 [jstype = JS_STRING]; + + /* + The minimum ppm fee in msat that the autopilot can set for a channel. + */ + uint32 min_rate_ppm = 3; + + /* + The maximum ppm fee in msat that the autopilot can set for a channel. + */ + uint32 max_rate_ppm = 4; + + /* + The minimum cltv delta that the autopilot may set for a channel. + */ + uint32 min_cltv_delta = 5; + + /* + The maximum cltv delta that the autopilot may set for a channel. + */ + uint32 max_cltv_delta = 6; + + /* + The minimum htlc msat that the autopilot may set for a channel. + */ + uint64 min_htlc_msat = 7 [jstype = JS_STRING]; + + /* + The maximum htlc msat that the autopilot may set for a channel. + */ + uint64 max_htlc_msat = 8 [jstype = JS_STRING]; +} + +message OffChainBudget { + uint64 max_amt_msat = 1 [jstype = JS_STRING]; + uint64 max_fees_msat = 2 [jstype = JS_STRING]; +} + +message OnChainBudget { + uint64 absolute_amt_sats = 1 [jstype = JS_STRING]; + uint64 max_sat_per_v_byte = 2 [jstype = JS_STRING]; +} + +message SendToSelf { +} + +message ChannelRestrict { + repeated uint64 channel_ids = 1 [jstype = JS_STRING]; +} + +message PeerRestrict { + repeated string peer_ids = 1; +} \ No newline at end of file diff --git a/rules/chan_policy_bounds.go b/rules/chan_policy_bounds.go new file mode 100644 index 000000000..b003d35a3 --- /dev/null +++ b/rules/chan_policy_bounds.go @@ -0,0 +1,334 @@ +package rules + +import ( + "context" + "fmt" + "math" + + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lightning-terminal/litrpc" + mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/protobuf/proto" +) + +var ( + // Compile-time checks to ensure that ChanPolicyBounds and + // ChanPolicyBoundsMgr implement the appropriate Manager, Enforcer and + // Values interface. + _ Manager = (*ChanPolicyBoundsMgr)(nil) + _ Enforcer = (*ChanPolicyBounds)(nil) + _ Values = (*ChanPolicyBounds)(nil) +) + +// ChanPolicyBoundsName is the string identifier of the ChanPolicyBounds rule. +const ChanPolicyBoundsName = "channel-policy-bounds" + +// ChanPolicyBoundsMgr manages the ChanPolicyBounds rule. +type ChanPolicyBoundsMgr struct{} + +// Stop cleans up the resources held by the manager. +// +// NOTE: This is part of the Manager interface. +func (b *ChanPolicyBoundsMgr) Stop() error { + return nil +} + +// NewEnforcer constructs a new ChanPolicyBounds rule enforcer using the passed +// values and config. +// +// NOTE: This is part of the Manager interface. +func (b *ChanPolicyBoundsMgr) NewEnforcer(_ Config, values Values) (Enforcer, + error) { + + bounds, ok := values.(*ChanPolicyBounds) + if !ok { + return nil, fmt.Errorf("values must be of type "+ + "ChanPolicyBounds, got %T", values) + } + + return bounds, nil +} + +// NewValueFromProto converts the given proto value into a ChanPolicyBounds +// Value object. +// +// NOTE: This is part of the Manager interface. +func (b *ChanPolicyBoundsMgr) NewValueFromProto(value *litrpc.RuleValue) ( + Values, error) { + + rv, ok := value.Value.(*litrpc.RuleValue_ChanPolicyBounds) + if !ok { + return nil, fmt.Errorf("incorrect RuleValue type") + } + + policyBounds := rv.ChanPolicyBounds + + return &ChanPolicyBounds{ + MinBaseMsat: policyBounds.MinBaseMsat, + MaxBaseMsat: policyBounds.MaxBaseMsat, + MinRatePPM: policyBounds.MinRatePpm, + MaxRatePPM: policyBounds.MaxRatePpm, + MinCLTVDelta: policyBounds.MinCltvDelta, + MaxCLTVDelta: policyBounds.MaxCltvDelta, + MinHtlcMsat: policyBounds.MinHtlcMsat, + MaxHtlcMsat: policyBounds.MaxHtlcMsat, + }, nil +} + +// EmptyValue returns a new instance of ChanPolicyBounds. +// +// NOTE: This is part of the Manager interface. +func (b *ChanPolicyBoundsMgr) EmptyValue() Values { + return &ChanPolicyBounds{} +} + +// ChanPolicyBounds represents the channel policy bounds rule. +type ChanPolicyBounds struct { + // MinBaseMsat is the minimum base fee in msat that can set for a + // channel. + MinBaseMsat uint64 `json:"min_base_msat"` + + // MaxBaseMsat is the maximum base fee in msat that can set for a + // channel. + MaxBaseMsat uint64 `json:"max_base_msat"` + + // MinRatePPM is the minimum ppm fee in msat that can be set for a + // channel. + MinRatePPM uint32 `json:"min_rate_ppm"` + + // MaxRatePPM is the maximum ppm fee in msat that can be set for a + // channel. + MaxRatePPM uint32 `json:"max_rate_ppm"` + + // MinCLTVDelta is the minimum cltv delta that may set for a channel. + MinCLTVDelta uint32 `json:"min_cltv_delta"` + + // MaxCLTVDelta is the maximum cltv delta that may set for a channel. + MaxCLTVDelta uint32 `json:"max_cltv_delta"` + + // MinHtlcMsat is the minimum htlc size msat that may set for a channel. + MinHtlcMsat uint64 `json:"min_htlc_msat"` + + // MaxHtlcMsat is the maximum htlc size in msat that may be set for a + // channel. + MaxHtlcMsat uint64 `json:"max_htlc_msat"` +} + +// HandleRequest checks the validity of a request using the ChanPolicyBounds +// rpcmiddleware.RoundTripCheckers. +// +// NOTE: this is part of the Enforcer interface. +func (f *ChanPolicyBounds) HandleRequest(ctx context.Context, uri string, + msg proto.Message) (proto.Message, error) { + + checkers := f.checkers() + if checkers == nil { + return nil, nil + } + + checker, ok := checkers[uri] + if !ok { + return nil, nil + } + + if !checker.HandlesRequest(msg.ProtoReflect().Type()) { + return nil, fmt.Errorf("invalid implementation, checker "+ + "for URI %s does not accept request of type %v", + uri, msg.ProtoReflect().Type()) + } + + return checker.HandleRequest(ctx, msg) +} + +// HandleResponse handles and possible alters a response. This is a noop for the +// ChanPolicyBounds rule. +// +// NOTE: this is part of the Enforcer interface. +func (f *ChanPolicyBounds) HandleResponse(_ context.Context, _ string, + _ proto.Message) (proto.Message, error) { + + return nil, nil +} + +// HandleErrorResponse handles and possible alters an error. This is a noop for +// the ChanPolicyBounds rule. +// +// NOTE: this is part of the Enforcer interface. +func (f *ChanPolicyBounds) HandleErrorResponse(_ context.Context, _ string, + _ error) (error, error) { + + return nil, nil +} + +// checkers returns a map of URI to rpcmiddleware.RoundTripChecker which define +// how the URI should be handled. +func (f *ChanPolicyBounds) checkers() map[string]mid.RoundTripChecker { + return map[string]mid.RoundTripChecker{ + "/lnrpc.Lightning/UpdateChannelPolicy": mid.NewRequestChecker( + &lnrpc.PolicyUpdateRequest{}, + &lnrpc.PolicyUpdateResponse{}, + func(ctx context.Context, + r *lnrpc.PolicyUpdateRequest) error { + + return f.checkPolicyUpdate(ctx, r) + }, + ), + } +} + +// checkPolicyUpdate verifies that the given lnrpc.PolicyUpdateRequest request +// is valid given the ChanPolicyBounds values. +func (f *ChanPolicyBounds) checkPolicyUpdate(_ context.Context, + req *lnrpc.PolicyUpdateRequest) error { + + if req.BaseFeeMsat < int64(f.MinBaseMsat) || + req.BaseFeeMsat > int64(f.MaxBaseMsat) { + + return fmt.Errorf("invalid base fee amount") + } + + if req.FeeRate == 0 && req.FeeRatePpm == 0 && f.MinRatePPM > 0 { + return fmt.Errorf("invalid fee rate") + } + + feeRate := req.FeeRatePpm + if req.FeeRate != 0 { + feeRate = uint32(math.Round(req.FeeRate * 1000000)) + } + + if feeRate < f.MinRatePPM || feeRate > f.MaxRatePPM { + return fmt.Errorf("invalid fee rate") + } + + if req.TimeLockDelta < f.MinCLTVDelta || + req.TimeLockDelta > f.MaxCLTVDelta { + + return fmt.Errorf("invalid cltv delta") + } + + if req.MinHtlcMsatSpecified { + if req.MinHtlcMsat < f.MinHtlcMsat { + return fmt.Errorf("invalid min htlc msat amount") + } + } + + if req.MaxHtlcMsat > f.MaxHtlcMsat { + return fmt.Errorf("invalid max htlc msat amount") + } + + return nil +} + +// VerifySane checks that the value of the values is ok given the min and max +// allowed values. +// +// NOTE: this is part of the Values interface. +func (f *ChanPolicyBounds) VerifySane(minVal, maxVal Values) error { + minFB, ok := minVal.(*ChanPolicyBounds) + if !ok { + return fmt.Errorf("min value is not of type ChanPolicyBounds") + } + + maxFB, ok := maxVal.(*ChanPolicyBounds) + if !ok { + return fmt.Errorf("max value is not of type ChanPolicyBounds") + } + + if !(f.MinBaseMsat >= minFB.MinBaseMsat && + f.MinBaseMsat <= maxFB.MinBaseMsat) { + + return fmt.Errorf("invalid min base fee") + } + + if !(f.MaxBaseMsat >= minFB.MaxBaseMsat && + f.MaxBaseMsat <= maxFB.MaxBaseMsat) { + + return fmt.Errorf("invalid max base fee") + } + + if !(f.MinRatePPM >= minFB.MinRatePPM && + f.MinRatePPM <= maxFB.MinRatePPM) { + + return fmt.Errorf("invalid min proportional fee") + } + + if !(f.MaxRatePPM >= minFB.MaxRatePPM && + f.MaxRatePPM <= maxFB.MaxRatePPM) { + + return fmt.Errorf("invalid max proportional fee") + } + + if !(f.MinCLTVDelta >= minFB.MinCLTVDelta && + f.MinCLTVDelta <= maxFB.MinCLTVDelta) { + + return fmt.Errorf("invalid min cltv delta") + } + + if !(f.MaxCLTVDelta >= minFB.MaxCLTVDelta && + f.MaxCLTVDelta <= maxFB.MaxCLTVDelta) { + + return fmt.Errorf("invalid max cltv delta") + } + + if !(f.MinHtlcMsat >= minFB.MinHtlcMsat && + f.MinHtlcMsat <= maxFB.MinHtlcMsat) { + + return fmt.Errorf("invalid min htlc msat amt") + } + + if !(f.MaxHtlcMsat >= minFB.MaxHtlcMsat && + f.MaxHtlcMsat <= maxFB.MaxHtlcMsat) { + + return fmt.Errorf("invalid max htlc msat amt") + } + + return nil +} + +// ToProto converts the rule Values to the litrpc counterpart. +// +// NOTE: this is part of the Values interface. +func (f *ChanPolicyBounds) ToProto() *litrpc.RuleValue { + return &litrpc.RuleValue{ + Value: &litrpc.RuleValue_ChanPolicyBounds{ + ChanPolicyBounds: &litrpc.ChannelPolicyBounds{ + MinBaseMsat: f.MinBaseMsat, + MaxBaseMsat: f.MaxBaseMsat, + MinRatePpm: f.MinRatePPM, + MaxRatePpm: f.MaxRatePPM, + MinCltvDelta: f.MinCLTVDelta, + MaxCltvDelta: f.MaxCLTVDelta, + MinHtlcMsat: f.MinHtlcMsat, + MaxHtlcMsat: f.MaxHtlcMsat, + }, + }, + } +} + +// RuleName returns the name of the rule that these values are to be used with. +// +// NOTE: this is part of the Values interface. +func (f *ChanPolicyBounds) RuleName() string { + return ChanPolicyBoundsName +} + +// PseudoToReal attempts to convert any appropriate pseudo fields in the rule +// Values to their corresponding real values. It uses the passed PrivacyMapDB to +// find the real values. This is a no-op for the ChanPolicyBounds rule. +// +// NOTE: this is part of the Values interface. +func (f *ChanPolicyBounds) PseudoToReal(_ firewalldb.PrivacyMapDB) (Values, + error) { + + return f, nil +} + +// RealToPseudo converts the rule Values to a new one that uses pseudo keys, +// channel IDs, channel points etc. It returns a map of real to pseudo strings +// that should be persisted. This is a no-op for the ChanPolicyBounds rule. +// +// NOTE: this is part of the Values interface. +func (f *ChanPolicyBounds) RealToPseudo() (Values, map[string]string, error) { + return f, nil, nil +} diff --git a/rules/chan_policy_bounds_test.go b/rules/chan_policy_bounds_test.go new file mode 100644 index 000000000..307045822 --- /dev/null +++ b/rules/chan_policy_bounds_test.go @@ -0,0 +1,385 @@ +package rules + +import ( + "context" + "fmt" + "testing" + + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// TestChanPolicyBoundsVerifySane tests that the ChanPolicyBounds VerifySane +// method correctly verifies the value of the channel policy bounds depending on +// given min and max sane values. +func TestChanPolicyBoundsVerifySane(t *testing.T) { + var ( + min = &ChanPolicyBounds{ + MinBaseMsat: 5, + MaxBaseMsat: 5, + MinRatePPM: 5, + MaxRatePPM: 5, + MinCLTVDelta: 5, + MaxCLTVDelta: 5, + MinHtlcMsat: 5, + MaxHtlcMsat: 5, + } + max = &ChanPolicyBounds{ + MinBaseMsat: 10, + MaxBaseMsat: 10, + MinRatePPM: 10, + MaxRatePPM: 10, + MinCLTVDelta: 10, + MaxCLTVDelta: 10, + MinHtlcMsat: 10, + MaxHtlcMsat: 10, + } + ) + + tests := []struct { + name string + rule *ChanPolicyBounds + expectErr error + }{ + { + name: "between bounds", + rule: &ChanPolicyBounds{ + MinBaseMsat: 6, + MaxBaseMsat: 6, + MinRatePPM: 6, + MaxRatePPM: 6, + MinCLTVDelta: 6, + MaxCLTVDelta: 6, + MinHtlcMsat: 6, + MaxHtlcMsat: 6, + }, + }, + { + name: "min base msat out of bounds", + rule: &ChanPolicyBounds{ + MinBaseMsat: 1, + MaxBaseMsat: 6, + MinRatePPM: 6, + MaxRatePPM: 6, + MinCLTVDelta: 6, + MaxCLTVDelta: 6, + MinHtlcMsat: 6, + MaxHtlcMsat: 6, + }, + expectErr: fmt.Errorf("invalid min base fee"), + }, + { + name: "max base msat out of bounds", + rule: &ChanPolicyBounds{ + MinBaseMsat: 6, + MaxBaseMsat: 11, + MinRatePPM: 6, + MaxRatePPM: 6, + MinCLTVDelta: 6, + MaxCLTVDelta: 6, + MinHtlcMsat: 6, + MaxHtlcMsat: 6, + }, + expectErr: fmt.Errorf("invalid max base fee"), + }, + { + name: "min prop fee out of bounds", + rule: &ChanPolicyBounds{ + MinBaseMsat: 6, + MaxBaseMsat: 6, + MinRatePPM: 3, + MaxRatePPM: 6, + MinCLTVDelta: 6, + MaxCLTVDelta: 6, + MinHtlcMsat: 6, + MaxHtlcMsat: 6, + }, + expectErr: fmt.Errorf("invalid min proportional fee"), + }, + { + name: "max prop fee out of bounds", + rule: &ChanPolicyBounds{ + MinBaseMsat: 6, + MaxBaseMsat: 6, + MinRatePPM: 6, + MaxRatePPM: 2, + MinCLTVDelta: 6, + MaxCLTVDelta: 6, + MinHtlcMsat: 6, + MaxHtlcMsat: 6, + }, + expectErr: fmt.Errorf("invalid max proportional fee"), + }, + { + name: "min cltv delta out of bounds", + rule: &ChanPolicyBounds{ + MinBaseMsat: 6, + MaxBaseMsat: 6, + MinRatePPM: 6, + MaxRatePPM: 6, + MinCLTVDelta: 1, + MaxCLTVDelta: 6, + MinHtlcMsat: 6, + MaxHtlcMsat: 6, + }, + expectErr: fmt.Errorf("invalid min cltv delta"), + }, + { + name: "max cltv delta out of bounds", + rule: &ChanPolicyBounds{ + MinBaseMsat: 6, + MaxBaseMsat: 6, + MinRatePPM: 6, + MaxRatePPM: 6, + MinCLTVDelta: 6, + MaxCLTVDelta: 30, + MinHtlcMsat: 6, + MaxHtlcMsat: 6, + }, + expectErr: fmt.Errorf("invalid max cltv delta"), + }, + { + name: "min htlc msat out of bounds", + rule: &ChanPolicyBounds{ + MinBaseMsat: 6, + MaxBaseMsat: 6, + MinRatePPM: 6, + MaxRatePPM: 6, + MinCLTVDelta: 6, + MaxCLTVDelta: 6, + MinHtlcMsat: 1, + MaxHtlcMsat: 6, + }, + expectErr: fmt.Errorf("invalid min htlc msat amt"), + }, + { + name: "max cltv delta of out bounds", + rule: &ChanPolicyBounds{ + MinBaseMsat: 6, + MaxBaseMsat: 6, + MinRatePPM: 6, + MaxRatePPM: 6, + MinCLTVDelta: 6, + MaxCLTVDelta: 6, + MinHtlcMsat: 6, + MaxHtlcMsat: 30, + }, + expectErr: fmt.Errorf("invalid max htlc msat amt"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.rule.VerifySane(min, max) + require.Equal(t, test.expectErr, err) + }) + } +} + +// TestChannelPolicyBoundsCheckers ensures that the ChanPolicyBounds values +// correctly accepts or denys a request. +func TestChannelPolicyBoundsCheckers(t *testing.T) { + values := &ChanPolicyBounds{ + MinBaseMsat: 5, + MaxBaseMsat: 10, + MinRatePPM: 5000000, + MaxRatePPM: 10000000, + MinCLTVDelta: 10, + MaxCLTVDelta: 40, + MinHtlcMsat: 10, + MaxHtlcMsat: 1000, + } + + tests := []struct { + name string + uri string + msg proto.Message + expectErr bool + }{ + { + name: "non policy update uri", + uri: "random-uri", + }, + { + name: "policy within bounds (using fee rate ppm)", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 6, + FeeRatePpm: 6000000, + TimeLockDelta: 20, + MinHtlcMsat: 20, + MaxHtlcMsat: 100, + MinHtlcMsatSpecified: true, + }, + }, + { + name: "policy within bounds (using fee rate)", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 6, + FeeRate: 6, + TimeLockDelta: 20, + MinHtlcMsat: 20, + MaxHtlcMsat: 100, + MinHtlcMsatSpecified: true, + }, + }, + { + name: "base fees too high", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 11, + FeeRate: 6, + TimeLockDelta: 20, + MinHtlcMsat: 20, + MaxHtlcMsat: 100, + MinHtlcMsatSpecified: true, + }, + expectErr: true, + }, + { + name: "base fees too low", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 4, + FeeRate: 6, + TimeLockDelta: 20, + MinHtlcMsat: 20, + MaxHtlcMsat: 100, + MinHtlcMsatSpecified: true, + }, + expectErr: true, + }, + { + name: "fee-rate too low (using fee-rate)", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 6, + FeeRate: 4, + TimeLockDelta: 20, + MinHtlcMsat: 20, + MaxHtlcMsat: 100, + MinHtlcMsatSpecified: true, + }, + expectErr: true, + }, + { + name: "fee-rate too high (using fee-rate)", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 6, + FeeRate: 11, + TimeLockDelta: 20, + MinHtlcMsat: 20, + MaxHtlcMsat: 100, + MinHtlcMsatSpecified: true, + }, + expectErr: true, + }, + { + name: "fee-rate too low (using fee-rate ppm)", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 6, + FeeRatePpm: 4000000, + TimeLockDelta: 20, + MinHtlcMsat: 20, + MaxHtlcMsat: 100, + MinHtlcMsatSpecified: true, + }, + expectErr: true, + }, + { + name: "fee-rate too high (using fee-rate ppm)", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 6, + FeeRatePpm: 11000000, + TimeLockDelta: 20, + MinHtlcMsat: 20, + MaxHtlcMsat: 100, + MinHtlcMsatSpecified: true, + }, + expectErr: true, + }, + { + name: "cltv delta too low", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 6, + FeeRatePpm: 6000000, + TimeLockDelta: 2, + MinHtlcMsat: 20, + MaxHtlcMsat: 100, + MinHtlcMsatSpecified: true, + }, + expectErr: true, + }, + { + name: "cltv delta too high", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 6, + FeeRatePpm: 6000000, + TimeLockDelta: 60, + MinHtlcMsat: 20, + MaxHtlcMsat: 100, + MinHtlcMsatSpecified: true, + }, + expectErr: true, + }, + { + name: "cltv delta too low", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 6, + FeeRatePpm: 6000000, + TimeLockDelta: 2, + MinHtlcMsat: 20, + MaxHtlcMsat: 100, + MinHtlcMsatSpecified: true, + }, + expectErr: true, + }, + { + name: "min htlc msat amt too low", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 6, + FeeRatePpm: 6000000, + TimeLockDelta: 60, + MinHtlcMsat: 2, + MaxHtlcMsat: 100, + MinHtlcMsatSpecified: true, + }, + expectErr: true, + }, + { + name: "max htlc msat amt too high", + uri: "/lnrpc.Lightning/UpdateChannelPolicy", + msg: &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: 6, + FeeRatePpm: 6000000, + TimeLockDelta: 60, + MinHtlcMsat: 20, + MaxHtlcMsat: 2000, + MinHtlcMsatSpecified: true, + }, + expectErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := values.HandleRequest( + context.Background(), test.uri, test.msg, + ) + if test.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/rules/channel_restrictions.go b/rules/channel_restrictions.go new file mode 100644 index 000000000..b642ab4bb --- /dev/null +++ b/rules/channel_restrictions.go @@ -0,0 +1,391 @@ +package rules + +import ( + "context" + "fmt" + "sync" + + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lightning-terminal/litrpc" + mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/protobuf/proto" +) + +var ( + // Compile-time checks to ensure that ChannelRestrict, + // ChannelRestrictMgr and ChannelRestrictEnforcer implement the + // appropriate Manager, Enforcer and Values interface. + _ Manager = (*ChannelRestrictMgr)(nil) + _ Enforcer = (*ChannelRestrictEnforcer)(nil) + _ Values = (*ChannelRestrict)(nil) +) + +// ChannelRestrictName is the string identifier of the ChannelRestrict rule. +const ChannelRestrictName = "channel-restriction" + +// ChannelRestrictMgr manages the ChannelRestrict rule. +type ChannelRestrictMgr struct { + // here we can have known chanID to ChanOutpoint map (and vice versa) + // then in NewEnforcer, if chan comes in we dont know of, then we + // refresh the maps. + + // chanIDToPoint is a map from channel ID to channel points for our + // known set of channels. + chanIDToPoint map[uint64]string + + // chanPointToID is a map from channel point to channel ID's for our + // known set of channels. + chanPointToID map[string]uint64 + mu sync.Mutex +} + +// NewChannelRestrictMgr constructs a new instance of a ChannelRestrictMgr. +func NewChannelRestrictMgr() *ChannelRestrictMgr { + return &ChannelRestrictMgr{ + chanIDToPoint: make(map[uint64]string), + chanPointToID: make(map[string]uint64), + } +} + +// Stop cleans up the resources held by the manager. +// +// NOTE: This is part of the Manager interface. +func (c *ChannelRestrictMgr) Stop() error { + return nil +} + +// NewEnforcer constructs a new ChannelRestrict rule enforcer using the passed +// values and config. +// +// NOTE: This is part of the Manager interface. +func (c *ChannelRestrictMgr) NewEnforcer(cfg Config, values Values) (Enforcer, + error) { + + channels, ok := values.(*ChannelRestrict) + if !ok { + return nil, fmt.Errorf("values must be of type "+ + "ChannelRestrict, got %T", values) + } + + chanMap := make(map[uint64]bool, len(channels.DenyList)) + for _, chanID := range channels.DenyList { + chanMap[chanID] = true + if err := c.maybeUpdateChannelMaps(cfg, chanID); err != nil { + return nil, err + } + } + + return &ChannelRestrictEnforcer{ + mgr: c, + ChannelRestrict: channels, + channelMap: chanMap, + }, nil +} + +// NewValueFromProto converts the given proto value into a ChannelRestrict Value +// object. +// +// NOTE: This is part of the Manager interface. +func (c *ChannelRestrictMgr) NewValueFromProto(v *litrpc.RuleValue) (Values, + error) { + + rv, ok := v.Value.(*litrpc.RuleValue_ChannelRestrict) + if !ok { + return nil, fmt.Errorf("incorrect RuleValue type") + } + + chanIDs := rv.ChannelRestrict.ChannelIds + + if len(chanIDs) == 0 { + return nil, fmt.Errorf("channel restrict list cannot be " + + "empty. If no channel restrictions should be applied " + + "then there is no need to add the rule") + } + + return &ChannelRestrict{ + DenyList: chanIDs, + }, nil +} + +// EmptyValue returns a new ChannelRestrict instance. +// +// NOTE: This is part of the Manager interface. +func (c *ChannelRestrictMgr) EmptyValue() Values { + return &ChannelRestrict{} +} + +// maybeUpdateChannelMaps updates the ChannelRestrictMgrs set of known channels +// iff the channel given by the caller is not found in the current map set. +func (c *ChannelRestrictMgr) maybeUpdateChannelMaps(cfg Config, + chanID uint64) error { + + c.mu.Lock() + defer c.mu.Unlock() + + // If we already know of this channel, we don't need to go update our + // maps. + _, ok := c.chanIDToPoint[chanID] + if ok { + return nil + } + + // Fetch a list of our open channels from LND. + lnd := cfg.GetLndClient() + chans, err := lnd.ListChannels(context.Background(), false, false) + if err != nil { + return err + } + + var ( + found bool + point string + id uint64 + ) + + // Update our set of maps and also make sure that the channel specified + // by the caller is valid given our set of open channels. + for _, channel := range chans { + point = channel.ChannelPoint + id = channel.ChannelID + + c.chanPointToID[point] = id + c.chanIDToPoint[id] = point + + if id == chanID { + found = true + } + } + + if !found { + return fmt.Errorf("invalid channel ID") + } + + return nil +} + +func (c *ChannelRestrictMgr) getChannelID(point string) (uint64, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + id, ok := c.chanPointToID[point] + return id, ok +} + +// ChannelRestrictEnforcer enforces requests and responses against a +// ChannelRestrict rule. +type ChannelRestrictEnforcer struct { + mgr *ChannelRestrictMgr + *ChannelRestrict + channelMap map[uint64]bool +} + +// HandleRequest checks the validity of a request using the ChannelRestrict +// rpcmiddleware.RoundTripCheckers. +// +// NOTE: this is part of the Enforcer interface. +func (c *ChannelRestrictEnforcer) HandleRequest(ctx context.Context, uri string, + msg proto.Message) (proto.Message, error) { + + checkers := c.checkers() + if checkers == nil { + return nil, nil + } + + checker, ok := checkers[uri] + if !ok { + return nil, nil + } + + if !checker.HandlesRequest(msg.ProtoReflect().Type()) { + return nil, fmt.Errorf("invalid implementation, checker for "+ + "URI %s does not accept request of type %v", uri, + msg.ProtoReflect().Type()) + } + + return checker.HandleRequest(ctx, msg) +} + +// HandleResponse handles a response using the ChannelRestrict +// rpcmiddleware.RoundTripCheckers. +// +// NOTE: this is part of the Enforcer interface. +func (c *ChannelRestrictEnforcer) HandleResponse(ctx context.Context, uri string, + msg proto.Message) (proto.Message, error) { + + checkers := c.checkers() + if checkers == nil { + return nil, nil + } + + checker, ok := checkers[uri] + if !ok { + return nil, nil + } + + if !checker.HandlesResponse(msg.ProtoReflect().Type()) { + return nil, fmt.Errorf("invalid implementation, checker for "+ + "URI %s does not accept response of type %v", uri, + msg.ProtoReflect().Type()) + } + + return checker.HandleResponse(ctx, msg) +} + +// HandleErrorResponse handles and possible alters an error. This is a noop for +// the ChannelRestrict rule. +// +// NOTE: this is part of the Enforcer interface. +func (c *ChannelRestrictEnforcer) HandleErrorResponse(_ context.Context, + _ string, _ error) (error, error) { + + return nil, nil +} + +// checkers returns a map of URI to rpcmiddleware.RoundTripChecker which define +// how the URI should be handled. +func (c *ChannelRestrictEnforcer) checkers() map[string]mid.RoundTripChecker { + return map[string]mid.RoundTripChecker{ + "/lnrpc.Lightning/UpdateChannelPolicy": mid.NewRequestChecker( + &lnrpc.PolicyUpdateRequest{}, + &lnrpc.PolicyUpdateResponse{}, + func(ctx context.Context, + r *lnrpc.PolicyUpdateRequest) error { + + if r.GetGlobal() { + return fmt.Errorf("cant apply call " + + "to global scope when using " + + "a channel restriction list") + } + + chanPoint := r.GetChanPoint() + if chanPoint == nil { + return fmt.Errorf("no channel point " + + "specified") + } + + txid, err := lnrpc.GetChanPointFundingTxid( + chanPoint, + ) + if err != nil { + return err + } + + index := chanPoint.GetOutputIndex() + point := fmt.Sprintf( + "%s:%d", txid.String(), index, + ) + + id, ok := c.mgr.getChannelID(point) + if !ok { + return nil + } + + if c.channelMap[id] { + return fmt.Errorf("illegal action on " + + "channel in channel " + + "restriction list") + } + + return nil + }, + ), + } +} + +// ChannelRestrict is a rule prevents calls from acting upon a given set of +// channels. +type ChannelRestrict struct { + // DenyList is a list of SCIDs that should not be acted upon by + // any call. + DenyList []uint64 `json:"channel_deny_list"` +} + +// VerifySane checks that the value of the values is ok given the min and max +// allowed values. This is a noop for the ChannelRestrict rule. +// +// NOTE: this is part of the Values interface. +func (c *ChannelRestrict) VerifySane(_, _ Values) error { + return nil +} + +// RuleName returns the name of the rule that these values are to be used with. +// +// NOTE: this is part of the Values interface. +func (c *ChannelRestrict) RuleName() string { + return ChannelRestrictName +} + +// ToProto converts the rule Values to the litrpc counterpart. +// +// NOTE: this is part of the Values interface. +func (c *ChannelRestrict) ToProto() *litrpc.RuleValue { + return &litrpc.RuleValue{ + Value: &litrpc.RuleValue_ChannelRestrict{ + ChannelRestrict: &litrpc.ChannelRestrict{ + ChannelIds: c.DenyList, + }, + }, + } +} + +// PseudoToReal assumes that the deny-list contains pseudo channel IDs and uses +// these to check the privacy map db for the corresponding real channel IDs. +// It constructs a new ChannelRestrict instance with these real channel IDs. +// +// NOTE: this is part of the Values interface. +func (c *ChannelRestrict) PseudoToReal(db firewalldb.PrivacyMapDB) (Values, + error) { + + restrictList := make([]uint64, len(c.DenyList)) + err := db.View(func(tx firewalldb.PrivacyMapTx) error { + for i, chanID := range c.DenyList { + real, err := firewalldb.RevealUint64(tx, chanID) + if err != nil { + return err + } + + restrictList[i] = real + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return &ChannelRestrict{ + DenyList: restrictList, + }, nil +} + +// RealToPseudo converts all the channel IDs into pseudo IDs. +// +// NOTE: this is part of the Values interface. +func (c *ChannelRestrict) RealToPseudo() (Values, map[string]string, error) { + pseudoIDs := make([]uint64, len(c.DenyList)) + privMapPairs := make(map[string]string) + for i, c := range c.DenyList { + // TODO(elle): check that this channel actually exists + + chanID := firewalldb.Uint64ToStr(c) + if pseudo, ok := privMapPairs[chanID]; ok { + p, err := firewalldb.StrToUint64(pseudo) + if err != nil { + return nil, nil, err + } + + pseudoIDs[i] = p + continue + } + + pseudoCp, pseudoCpStr := firewalldb.NewPseudoUint64() + privMapPairs[chanID] = pseudoCpStr + pseudoIDs[i] = pseudoCp + } + + return &ChannelRestrict{ + DenyList: pseudoIDs, + }, privMapPairs, nil +} diff --git a/rules/channel_restrictions_test.go b/rules/channel_restrictions_test.go new file mode 100644 index 000000000..8bb5c811e --- /dev/null +++ b/rules/channel_restrictions_test.go @@ -0,0 +1,163 @@ +package rules + +import ( + "context" + "encoding/hex" + "fmt" + "math/rand" + "testing" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/stretchr/testify/require" +) + +// TestChannelRestrictCheckRequest ensures that the ChannelRestrictEnforcer +// correctly accepts or denys a request. +func TestChannelRestrictCheckRequest(t *testing.T) { + txid1, index1, err := newTXID() + require.NoError(t, err) + + txid2, index2, err := newTXID() + require.NoError(t, err) + + txid3, index3, err := newTXID() + require.NoError(t, err) + + chanPointStr1 := fmt.Sprintf("%s:%d", hex.EncodeToString(txid1), index1) + chanPointStr2 := fmt.Sprintf("%s:%d", hex.EncodeToString(txid2), index2) + chanPointStr3 := fmt.Sprintf("%s:%d", hex.EncodeToString(txid3), index3) + + chanID1, _ := firewalldb.NewPseudoUint64() + chanID2, _ := firewalldb.NewPseudoUint64() + chanID3, _ := firewalldb.NewPseudoUint64() + + ctx := context.Background() + mgr := NewChannelRestrictMgr() + cfg := &mockLndClient{ + channels: []lndclient.ChannelInfo{ + { + ChannelID: chanID1, + ChannelPoint: chanPointStr1, + }, + { + ChannelID: chanID2, + ChannelPoint: chanPointStr2, + }, + { + ChannelID: chanID3, + ChannelPoint: chanPointStr3, + }, + }, + } + enf, err := mgr.NewEnforcer(cfg, &ChannelRestrict{ + DenyList: []uint64{ + chanID1, chanID2, + }, + }) + require.NoError(t, err) + + // A request for an irrelevant URI should be allowed. + _, err = enf.HandleRequest(ctx, "random-URI", nil) + require.NoError(t, err) + + // If there is a channel restriction list, then no global policy updates + // are allowed. + _, err = enf.HandleRequest( + ctx, "/lnrpc.Lightning/UpdateChannelPolicy", + &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_Global{Global: true}, + }, + ) + require.ErrorContainsf(t, err, "cant apply call to global scope when "+ + "using a channel restriction list", "") + + // Test that an action on channel point 1 in the string form is + // disallowed. + chanPoint1 := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: hex.EncodeToString(txid1), + }, + OutputIndex: index1, + } + + _, err = enf.HandleRequest( + ctx, "/lnrpc.Lightning/UpdateChannelPolicy", + &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint1, + }, + }, + ) + require.ErrorContainsf(t, err, "illegal action on channel in channel "+ + "restriction list", "") + + // Test that an action on channel point 2 in the byte form is + // disallowed. + h, err := chainhash.NewHashFromStr(hex.EncodeToString(txid2)) + require.NoError(t, err) + + chanPoint2 := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ + FundingTxidBytes: h[:], + }, + OutputIndex: index2, + } + + _, err = enf.HandleRequest( + ctx, "/lnrpc.Lightning/UpdateChannelPolicy", + &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint2, + }, + }, + ) + require.ErrorContainsf(t, err, "illegal action on channel in channel "+ + "restriction list", "") + + // Test that an action on a channel not in the deny-list is allowed. + chanPoint3 := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: hex.EncodeToString(txid3), + }, + OutputIndex: index3, + } + + _, err = enf.HandleRequest( + ctx, "/lnrpc.Lightning/UpdateChannelPolicy", + &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint3, + }, + }, + ) + require.NoError(t, err) +} + +func newTXID() ([]byte, uint32, error) { + var b [32]byte + if _, err := rand.Read(b[:]); err != nil { + return nil, 0, err + } + + return b[:], rand.Uint32(), nil +} + +type mockLndClient struct { + lndclient.LightningClient + Config + + channels []lndclient.ChannelInfo +} + +func (m *mockLndClient) GetLndClient() lndclient.LightningClient { + return m +} + +func (m *mockLndClient) ListChannels(_ context.Context, _, _ bool) ( + []lndclient.ChannelInfo, error) { + + return m.channels, nil +} diff --git a/rules/config.go b/rules/config.go new file mode 100644 index 000000000..8a79b1545 --- /dev/null +++ b/rules/config.go @@ -0,0 +1,105 @@ +package rules + +import ( + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lndclient" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +// Config encompasses all the possible configuration items that could be +// required by the various rules. +type Config interface { + // GetStores can be used to get access to methods that can be used to + // perform atomic transactions on permanent and temporary local and + // global kv stores. + GetStores() firewalldb.KVStores + + // GetActionsDB can be used by rules to list any past actions that were + // made for the specific session or feature. + GetActionsDB() firewalldb.ActionsDB + + // GetMethodPerms returns a map that contains URIs and the permissions + // required to use them. + GetMethodPerms() func(string) ([]bakery.Op, bool) + + // GetNodePubKey returns the node ID of the lnd node. + GetNodePubKey() [33]byte + + // GetRouterClient returns an lnd router client. + GetRouterClient() lndclient.RouterClient + + // GetReqID is the request ID of the call being evaluated. This can be + // used to link a request with a response. + GetReqID() int64 + + // GetLndClient returns an lnd client. + GetLndClient() lndclient.LightningClient +} + +// ConfigImpl is an implementation of the Config interface. +type ConfigImpl struct { + // GetStores provides access to methods that can be used to perform + // atomic transactions on permanent and temporary local and global + // kv stores. + Stores firewalldb.KVStores + + // ActionsDB can be used by rules to list any past actions that were + // made for the specific session or feature. + ActionsDB firewalldb.ActionsDB + + // MethodPerms is a function that can be used to fetch the permissions + // required for a URI. + MethodPerms func(string) ([]bakery.Op, bool) + + // NodeID is the pub key of the lnd node. + NodeID [33]byte + + // RouterClient is an lnd router client. + RouterClient lndclient.RouterClient + + // ReqID is the request ID of the call being evaluated. This can be used + // to link a request with a response. + ReqID int64 + + // LndClient is a connection to the Lit node's LND node. + LndClient lndclient.LightningClient +} + +func (c *ConfigImpl) GetStores() firewalldb.KVStores { + return c.Stores +} + +// GetActionsDB returns the list of past actions. +func (c *ConfigImpl) GetActionsDB() firewalldb.ActionsDB { + return c.ActionsDB +} + +// GetMethodPerms returns a function that can be used to fetch the permissions +// of a URI. +func (c *ConfigImpl) GetMethodPerms() func(string) ([]bakery.Op, bool) { + return c.MethodPerms +} + +// GetNodePubKey returns the node ID for the lnd node. +func (c *ConfigImpl) GetNodePubKey() [33]byte { + return c.NodeID +} + +// GetRouterClient returns an lnd router client. +func (c *ConfigImpl) GetRouterClient() lndclient.RouterClient { + return c.RouterClient +} + +// GetReqID returns the request ID of the request or response being evaluated. +func (c *ConfigImpl) GetReqID() int64 { + return c.ReqID +} + +// GetLndClient returns an lnd client. +func (c *ConfigImpl) GetLndClient() lndclient.LightningClient { + return c.LndClient +} + +// A compile-time check to ensure that ConfigImpl implements the Config +// interface. +var _ Config = (*ConfigImpl)(nil) diff --git a/rules/history_limit.go b/rules/history_limit.go new file mode 100644 index 000000000..1c61ba4ae --- /dev/null +++ b/rules/history_limit.go @@ -0,0 +1,271 @@ +package rules + +import ( + "context" + "fmt" + "time" + + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lightning-terminal/litrpc" + mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/protobuf/proto" +) + +var ( + // Compile-time checks to ensure that HistoryLimit and HistoryLimitMgr + // implement the appropriate Manager, Enforcer and Values interface. + _ Manager = (*HistoryLimitMgr)(nil) + _ Enforcer = (*HistoryLimit)(nil) + _ Values = (*HistoryLimit)(nil) +) + +// HistoryLimitName is the string identifier of the HistoryLimit rule. +const HistoryLimitName = "history-limit" + +// HistoryLimitMgr manages the History limit rule. +type HistoryLimitMgr struct{} + +// Stop cleans up the resources held by the manager. +// +// NOTE: This is part of the Manager interface. +func (h *HistoryLimitMgr) Stop() error { + return nil +} + +// NewEnforcer constructs a new HistoryLimit rule enforcer using the passed +// values and config. +// +// NOTE: This is part of the Manager interface. +func (h *HistoryLimitMgr) NewEnforcer(_ Config, values Values) (Enforcer, + error) { + + limit, ok := values.(*HistoryLimit) + if !ok { + return nil, fmt.Errorf("values must be of type HistoryLimit, "+ + "got %T", values) + } + + return limit, nil +} + +// NewValueFromProto converts the given proto value into a HistoryLimit Value +// object. +// +// NOTE: This is part of the Manager interface. +func (h *HistoryLimitMgr) NewValueFromProto(v *litrpc.RuleValue) (Values, + error) { + + rv, ok := v.Value.(*litrpc.RuleValue_HistoryLimit) + if !ok { + return nil, fmt.Errorf("incorrect RuleValue type") + } + + historyLimit := rv.HistoryLimit + + if historyLimit.StartTime != 0 && historyLimit.Duration != 0 { + return nil, fmt.Errorf("cant set both start time and duration") + } + + if historyLimit.StartTime != 0 { + return &HistoryLimit{ + StartDate: time.Unix(int64(historyLimit.StartTime), 0), + }, nil + } + + return &HistoryLimit{ + Duration: time.Second * time.Duration(historyLimit.Duration), + }, nil +} + +// EmptyValue returns a new HistoryLimit instance. +// +// NOTE: This is part of the Manager interface. +func (h *HistoryLimitMgr) EmptyValue() Values { + return &HistoryLimit{} +} + +// HistoryLimit represents the history-limit values. +type HistoryLimit struct { + StartDate time.Time `json:"start_date,omitempty"` + Duration time.Duration `json:"duration,omitempty"` +} + +// HandleRequest checks the validity of a request using the HistoryLimit +// rpcmiddleware.RoundTripCheckers. +// +// NOTE: this is part of the Rule interface. +func (h *HistoryLimit) HandleRequest(ctx context.Context, uri string, + msg proto.Message) (proto.Message, error) { + + checkers := h.checkers() + if checkers == nil { + return nil, nil + } + + checker, ok := checkers[uri] + if !ok { + return nil, nil + } + + if !checker.HandlesRequest(msg.ProtoReflect().Type()) { + return nil, fmt.Errorf("invalid implementation, checker for "+ + "URI %s does not accept request of type %v", uri, + msg.ProtoReflect().Type()) + } + + return checker.HandleRequest(ctx, msg) +} + +// HandleResponse handles a response using the HistoryLimit +// rpcmiddleware.RoundTripCheckers. +// +// NOTE: this is part of the Rule interface. +func (h *HistoryLimit) HandleResponse(ctx context.Context, uri string, + msg proto.Message) (proto.Message, error) { + + checkers := h.checkers() + if checkers == nil { + return nil, nil + } + + checker, ok := checkers[uri] + if !ok { + return nil, nil + } + + if !checker.HandlesResponse(msg.ProtoReflect().Type()) { + return nil, fmt.Errorf("invalid implementation, checker for "+ + "URI %s does not accept response of type %v", uri, + msg.ProtoReflect().Type()) + } + + return checker.HandleResponse(ctx, msg) +} + +// HandleErrorResponse handles and possible alters an error. This is a noop for +// the HistoryLimit rule. +// +// NOTE: this is part of the Enforcer interface. +func (h *HistoryLimit) HandleErrorResponse(_ context.Context, _ string, + _ error) (error, error) { + + return nil, nil +} + +// checkers returns a map of URI to rpcmiddleware.RoundTripChecker which define +// how the URI should be handled. +func (h *HistoryLimit) checkers() map[string]mid.RoundTripChecker { + return map[string]mid.RoundTripChecker{ + "/lnrpc.Lightning/ForwardingHistory": mid.NewRequestChecker( + &lnrpc.ForwardingHistoryRequest{}, + &lnrpc.ForwardingHistoryResponse{}, + func(ctx context.Context, + r *lnrpc.ForwardingHistoryRequest) error { + + startDate := h.GetStartDate() + + if r.StartTime >= uint64(startDate.Unix()) { + return nil + } + + return fmt.Errorf("can't request a start "+ + "time before %s", startDate) + }, + ), + "lnrpc.Lightning/ListInvoices": mid.NewResponseRewriter( + &lnrpc.ListInvoiceRequest{}, + &lnrpc.ListInvoiceResponse{}, + func(ctx context.Context, + r *lnrpc.ListInvoiceResponse) (proto.Message, + error) { + + startDate := h.GetStartDate() + var invoices []*lnrpc.Invoice + for _, i := range r.Invoices { + if i.CreationDate < startDate.Unix() { + continue + } + + invoices = append(invoices, i) + } + + r.Invoices = invoices + return r, nil + }, mid.PassThroughErrorHandler, + ), + } +} + +// VerifySane checks that the value of the values is ok given the min and max +// allowed values. +// +// NOTE: this is part of the Values interface. +func (h *HistoryLimit) VerifySane(minVal, _ Values) error { + minHL, ok := minVal.(*HistoryLimit) + if !ok { + return fmt.Errorf("min value is not of type HistoryLimit") + } + + if !h.GetStartDate().Before(minHL.GetStartDate()) { + minDur := time.Since(minHL.GetStartDate()) + return fmt.Errorf("history-limit start date not valid for "+ + "given the minimum required start date. Start date "+ + "should at least be %s or the duration should at "+ + "least be %s", minHL.GetStartDate(), minDur) + } + + return nil +} + +// RuleName returns the name of the rule that these values are to be used with. +// +// NOTE: this is part of the Values interface. +func (h *HistoryLimit) RuleName() string { + return HistoryLimitName +} + +// ToProto converts the rule Values to the litrpc counterpart. +// +// NOTE: this is part of the Values interface. +func (h *HistoryLimit) ToProto() *litrpc.RuleValue { + return &litrpc.RuleValue{ + Value: &litrpc.RuleValue_HistoryLimit{ + HistoryLimit: &litrpc.HistoryLimit{ + StartTime: uint64(h.StartDate.Unix()), + Duration: uint64(h.Duration.Seconds()), + }, + }, + } +} + +// GetStartDate is a helper function that determines the start date of the values +// given if a start date is set or a max duration is given. +func (h *HistoryLimit) GetStartDate() time.Time { + startDate := h.StartDate + if h.StartDate.IsZero() { + startDate = time.Now().Add(-h.Duration) + } + + return startDate +} + +// PseudoToReal attempts to convert any appropriate pseudo fields in the rule +// Values to their corresponding real values. It uses the passed PrivacyMapDB to +// find the real values. This is a no-op for the HistoryLimit rule. +// +// NOTE: this is part of the Values interface. +func (h *HistoryLimit) PseudoToReal(_ firewalldb.PrivacyMapDB) (Values, + error) { + + return h, nil +} + +// RealToPseudo converts the rule Values to a new one that uses pseudo keys, +// channel IDs, channel points etc. It returns a map of real to pseudo strings +// that should be persisted. This is a no-op for the HistoryLimit rule. +// +// NOTE: this is part of the Values interface. +func (h *HistoryLimit) RealToPseudo() (Values, map[string]string, error) { + return h, nil, nil +} diff --git a/rules/history_limit_test.go b/rules/history_limit_test.go new file mode 100644 index 000000000..eb1dacc52 --- /dev/null +++ b/rules/history_limit_test.go @@ -0,0 +1,130 @@ +package rules + +import ( + "context" + "testing" + "time" + + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/stretchr/testify/require" +) + +// TestHistoryLimitVerifySane tests that the HistoryLimit VerifySane method +// correctly verifies the value of the rate limit depending on given min and +// max sane values. +func TestHistoryLimitVerifySane(t *testing.T) { + var min = &HistoryLimit{ + Duration: time.Hour * 24, + } + + tests := []struct { + name string + rule *HistoryLimit + expectErr bool + }{ + { + name: "between bounds (start date)", + rule: &HistoryLimit{ + StartDate: time.Now().Add(-time.Hour * 48), + }, + }, + { + name: "between bounds (duration)", + rule: &HistoryLimit{ + Duration: time.Hour * 48, + }, + }, + { + name: "too short (start date)", + rule: &HistoryLimit{ + StartDate: time.Now().Add(-time.Hour), + }, + expectErr: true, + }, + { + name: "too short (duration)", + rule: &HistoryLimit{ + Duration: time.Hour, + }, + expectErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.rule.VerifySane(min, nil) + if test.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +// TestHistoryLimitCheckers ensures that the HistoryLimit values correctly +// accepts or denys a request or correctly modifies a response. +func TestHistoryLimitCheckers(t *testing.T) { + values := &HistoryLimit{ + StartDate: time.Now().Add(-24 * time.Hour), + } + + ctx := context.Background() + + // A request for an irrelevant URI should be accepted. + _, err := values.HandleRequest(ctx, "random-URI", nil) + require.NoError(t, err) + + // The ForwardingHistory request has a StartTime parameter. The request + // should be allowed if the parameter is ok given the HistoryLimit values. + _, err = values.HandleRequest( + ctx, "/lnrpc.Lightning/ForwardingHistory", + &lnrpc.ForwardingHistoryRequest{ + StartTime: uint64(time.Now().Add(-time.Hour).Unix()), + }, + ) + require.NoError(t, err) + + // And it should be denied if it violates the values. + // The ForwardingHistory request has a StartTime parameter. The request + // should be allowed if the parameter is ok given the HistoryLimit values. + _, err = values.HandleRequest( + ctx, "/lnrpc.Lightning/ForwardingHistory", + &lnrpc.ForwardingHistoryRequest{ + StartTime: uint64( + time.Now().Add(-48 * time.Hour).Unix(), + ), + }, + ) + require.Error(t, err) + + // The ListInvoices function does not have a StartTime parameter and + // so the HistoryLimit values needs to alter the _response_ of this query + // instead to only include the invoices created after the HistoryLimit + // start date. + invoices := []*lnrpc.Invoice{ + {CreationDate: time.Now().Unix()}, + {CreationDate: time.Now().Add(-time.Hour * 5).Unix()}, + {CreationDate: time.Now().Add(-time.Hour * 25).Unix()}, + } + + respMsg, err := values.HandleResponse( + ctx, "lnrpc.Lightning/ListInvoices", + &lnrpc.ListInvoiceResponse{ + Invoices: invoices, + }, + ) + require.NoError(t, err) + require.NotNil(t, respMsg) + + resp, ok := respMsg.(*lnrpc.ListInvoiceResponse) + require.True(t, ok) + + require.Len(t, resp.Invoices, 2) + require.True(t, time.Unix(resp.Invoices[0].CreationDate, 0).After( + values.StartDate, + )) + require.True(t, time.Unix(resp.Invoices[1].CreationDate, 0).After( + values.StartDate, + )) +} diff --git a/rules/interfaces.go b/rules/interfaces.go new file mode 100644 index 000000000..3d2084bd9 --- /dev/null +++ b/rules/interfaces.go @@ -0,0 +1,75 @@ +package rules + +import ( + "context" + "encoding/json" + + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lightning-terminal/litrpc" + "google.golang.org/protobuf/proto" +) + +// Manager is the interface that any firewall rule managers will need to +// implement. A rule Manager is used to construct a rule Enforcer or rule +// Values. +type Manager interface { + // NewEnforcer constructs a new rule enforcer using the passed values + // and config. + NewEnforcer(cfg Config, values Values) (Enforcer, error) + + // NewValueFromProto converts the given proto value into a Value object. + NewValueFromProto(p *litrpc.RuleValue) (Values, error) + + // EmptyValue returns a new Values instance of the type that this + // Manager handles. + EmptyValue() Values + + // Stop cleans up the resources held by the manager. + Stop() error +} + +// Enforcer is the interface that any firewall rule enforcer must implement. +// An enforcer accepts, rejects, and possible alters an RPC proto message for a +// specific URI. +type Enforcer interface { + // HandleRequest checks the validity of a request and possibly edits it. + HandleRequest(ctx context.Context, uri string, + protoMsg proto.Message) (proto.Message, error) + + // HandleResponse handles and possibly alters a response. + HandleResponse(ctx context.Context, uri string, + protoMsg proto.Message) (proto.Message, error) + + // HandleErrorResponse handles and possibly alters a response error. + HandleErrorResponse(ctx context.Context, uri string, err error) (error, + error) +} + +// Values represents the static values that encompass the settings of the rule. +type Values interface { + // RuleName returns the name of the rule that these values are to be + // used with. + RuleName() string + + // VerifySane checks that the rules values are valid given the allowed + // minimum and maximum values. + VerifySane(minVal, maxVal Values) error + + // ToProto converts the rule Values to the litrpc counterpart. + ToProto() *litrpc.RuleValue + + // RealToPseudo converts the rule Values to a new one that uses pseudo + // keys, channel IDs, channel points etc. It returns a map of real to + // pseudo strings that should be persisted. + RealToPseudo() (Values, map[string]string, error) + + // PseudoToReal attempts to convert any appropriate pseudo fields in + // the rule Values to their corresponding real values. It uses the + // passed PrivacyMapDB to find the real values. + PseudoToReal(db firewalldb.PrivacyMapDB) (Values, error) +} + +// Marshal converts the rule Values to a json byte slice. +func Marshal(v Values) ([]byte, error) { + return json.Marshal(v) +} diff --git a/rules/log.go b/rules/log.go new file mode 100644 index 000000000..b1d5d13c3 --- /dev/null +++ b/rules/log.go @@ -0,0 +1,25 @@ +package rules + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +const Subsystem = "RULE" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger // nolint:unused + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/rules/manager_set.go b/rules/manager_set.go new file mode 100644 index 000000000..027bb602b --- /dev/null +++ b/rules/manager_set.go @@ -0,0 +1,92 @@ +package rules + +import ( + "encoding/json" + "fmt" + + "github.com/lightninglabs/lightning-terminal/litrpc" +) + +// ErrUnknownRule indicates that LiT is unaware of a values name. +var ErrUnknownRule = fmt.Errorf("unknown rule") + +// ManagerSet is a map from a rule name to a rule Manager. +type ManagerSet map[string]Manager + +// NewRuleManagerSet creates a new map of the supported rule ManagerSet. +func NewRuleManagerSet() ManagerSet { + return map[string]Manager{ + RateLimitName: &RateLimitMgr{}, + ChanPolicyBoundsName: &ChanPolicyBoundsMgr{}, + HistoryLimitName: &HistoryLimitMgr{}, + ChannelRestrictName: NewChannelRestrictMgr(), + PeersRestrictName: NewPeerRestrictMgr(), + } +} + +// InitEnforcer gets the appropriate rule Manager for the given name and uses it +// to create an appropriate rule Enforcer. +func (m ManagerSet) InitEnforcer(cfg Config, name string, + values Values) (Enforcer, error) { + + mgr, ok := m[name] + if !ok { + return nil, ErrUnknownRule + } + + return mgr.NewEnforcer(cfg, values) +} + +// GetAllRules returns a map of names of all the rules supported by rule +// ManagerSet. +func (m ManagerSet) GetAllRules() map[string]bool { + rules := make(map[string]bool, len(m)) + for name := range m { + rules[name] = true + } + return rules +} + +// UnmarshalRuleValues identifies the appropriate rule Manager based on the +// given rule name and uses that to parse the proto value into a Value object. +func (m ManagerSet) UnmarshalRuleValues(name string, proto *litrpc.RuleValue) ( + Values, error) { + + mgr, ok := m[name] + if !ok { + return nil, ErrUnknownRule + } + + return mgr.NewValueFromProto(proto) +} + +// InitRuleValues can be used to construct a Values object given raw rule +// value bytes along with the name of the appropriate rule. +func (m ManagerSet) InitRuleValues(name string, valueBytes []byte) (Values, + error) { + + mgr, ok := m[name] + if !ok { + return nil, ErrUnknownRule + } + + v := mgr.EmptyValue() + if err := json.Unmarshal(valueBytes, v); err != nil { + return nil, err + } + + return v, nil +} + +// Stop stops all the managers in the set. +func (m ManagerSet) Stop() error { + var returnErr error + for _, mgr := range m { + err := mgr.Stop() + if err != nil { + returnErr = err + } + } + + return returnErr +} diff --git a/rules/peer_restrictions.go b/rules/peer_restrictions.go new file mode 100644 index 000000000..9d3b287b8 --- /dev/null +++ b/rules/peer_restrictions.go @@ -0,0 +1,395 @@ +package rules + +import ( + "context" + "fmt" + "sync" + + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lightning-terminal/litrpc" + mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" + "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/protobuf/proto" +) + +var ( + // Compile-time checks to ensure that PeerRestrictMgr, + // PeerRestrict and PeerRestrictEnforcer implement the + // appropriate Manager, Enforcer and Values interface. + _ Manager = (*PeerRestrictMgr)(nil) + _ Enforcer = (*PeerRestrictEnforcer)(nil) + _ Values = (*PeerRestrict)(nil) +) + +// PeersRestrictName is the string identifier of the PeerRestrict rule. +const PeersRestrictName = "peer-restriction" + +// PeerRestrictMgr manages the PeerRestrict rule. +type PeerRestrictMgr struct { + chanPointToPeerID map[string]string + peerIDToChanPoint map[string]map[string]bool + mu sync.Mutex +} + +// NewPeerRestrictMgr constructs a new PeerRestrictMgr. +func NewPeerRestrictMgr() *PeerRestrictMgr { + return &PeerRestrictMgr{ + chanPointToPeerID: make(map[string]string), + peerIDToChanPoint: make(map[string]map[string]bool), + } +} + +// Stop cleans up the resources held by the manager. +// +// NOTE: This is part of the Manager interface. +func (c *PeerRestrictMgr) Stop() error { + return nil +} + +// NewEnforcer constructs a new PeerRestrict rule enforcer using the passed +// values and config. +// +// NOTE: This is part of the Manager interface. +func (c *PeerRestrictMgr) NewEnforcer(cfg Config, values Values) (Enforcer, + error) { + + peers, ok := values.(*PeerRestrict) + if !ok { + return nil, fmt.Errorf("values must be of type "+ + "PeerRestrict, got %T", values) + } + + peerMap := make(map[string]bool, len(peers.DenyList)) + for _, peerID := range peers.DenyList { + peerMap[peerID] = true + if err := c.maybeUpdateMaps(cfg, peerID); err != nil { + return nil, err + } + } + + return &PeerRestrictEnforcer{ + cfg: cfg, + mgr: c, + PeerRestrict: peers, + peerMap: peerMap, + }, nil +} + +// NewValueFromProto converts the given proto value into a PeerRestrict Value +// object. +// +// NOTE: This is part of the Manager interface. +func (c *PeerRestrictMgr) NewValueFromProto(v *litrpc.RuleValue) (Values, + error) { + + rv, ok := v.Value.(*litrpc.RuleValue_PeerRestrict) + if !ok { + return nil, fmt.Errorf("incorrect RuleValue type") + } + + peerIDs := rv.PeerRestrict.PeerIds + + if len(peerIDs) == 0 { + return nil, fmt.Errorf("peer restrict list cannot be " + + "empty. If no channel restrictions should be applied " + + "then there is no need to add the rule") + } + + return &PeerRestrict{ + DenyList: peerIDs, + }, nil +} + +// EmptyValue returns a new PeerRestrict instance. +// +// NOTE: This is part of the Manager interface. +func (c *PeerRestrictMgr) EmptyValue() Values { + return &PeerRestrict{} +} + +// maybeUpdateMaps updates the managers peer-to-channel and channel-to-peer maps +// if the given peer ID is unknown to the manager. +func (c *PeerRestrictMgr) maybeUpdateMaps(cfg peerRestrictCfg, + id string) error { + + c.mu.Lock() + defer c.mu.Unlock() + + if _, ok := c.peerIDToChanPoint[id]; ok { + return nil + } + + return c.updateMapsUnsafe(cfg) +} + +// updateMapsUnsafe updates the manager's peer-to-channel and channel-to-peer +// maps. It is not thread safe and so must only be called if the manager's +// mutex is being held. +func (c *PeerRestrictMgr) updateMapsUnsafe(cfg peerRestrictCfg) error { + lnd := cfg.GetLndClient() + chans, err := lnd.ListChannels(context.Background(), false, false) + if err != nil { + return err + } + + c.chanPointToPeerID = make(map[string]string) + c.peerIDToChanPoint = make(map[string]map[string]bool) + + for _, channel := range chans { + peerID := channel.PubKeyBytes.String() + _, ok := c.peerIDToChanPoint[peerID] + if !ok { + c.peerIDToChanPoint[peerID] = make(map[string]bool) + } + + c.peerIDToChanPoint[peerID][channel.ChannelPoint] = true + c.chanPointToPeerID[channel.ChannelPoint] = peerID + } + + return nil +} + +func (c *PeerRestrictMgr) getPeerFromChanPoint(cfg peerRestrictCfg, + cp string) (string, bool, error) { + + c.mu.Lock() + defer c.mu.Unlock() + + peer, ok := c.chanPointToPeerID[cp] + if ok { + return peer, ok, nil + } + + err := c.updateMapsUnsafe(cfg) + if err != nil { + return "", false, err + } + + peer, ok = c.chanPointToPeerID[cp] + return peer, ok, nil +} + +// peerRestrictCfg is the config required by PeerRestrictMgr. It can be derived +// from the main rules Config struct. +type peerRestrictCfg interface { + GetLndClient() lndclient.LightningClient +} + +// PeerRestrictEnforcer enforces requests and responses against a PeerRestrict +// rule. +type PeerRestrictEnforcer struct { + mgr *PeerRestrictMgr + cfg peerRestrictCfg + *PeerRestrict + + peerMap map[string]bool +} + +// HandleRequest checks the validity of a request using the PeerRestrict +// rpcmiddleware.RoundTripCheckers. +// +// NOTE: this is part of the Enforcer interface. +func (c *PeerRestrictEnforcer) HandleRequest(ctx context.Context, uri string, + msg proto.Message) (proto.Message, error) { + + checkers := c.checkers() + if checkers == nil { + return nil, nil + } + + checker, ok := checkers[uri] + if !ok { + return nil, nil + } + + if !checker.HandlesRequest(msg.ProtoReflect().Type()) { + return nil, fmt.Errorf("invalid implementation, checker for "+ + "URI %s does not accept request of type %v", uri, + msg.ProtoReflect().Type()) + } + + return checker.HandleRequest(ctx, msg) +} + +// HandleResponse handles a response using the PeerRestrict +// rpcmiddleware.RoundTripCheckers. +// +// NOTE: this is part of the Enforcer interface. +func (c *PeerRestrictEnforcer) HandleResponse(ctx context.Context, uri string, + msg proto.Message) (proto.Message, error) { + + checkers := c.checkers() + if checkers == nil { + return nil, nil + } + + checker, ok := checkers[uri] + if !ok { + return nil, nil + } + + if !checker.HandlesResponse(msg.ProtoReflect().Type()) { + return nil, fmt.Errorf("invalid implementation, checker for "+ + "URI %s does not accept response of type %v", uri, + msg.ProtoReflect().Type()) + } + + return checker.HandleResponse(ctx, msg) +} + +// HandleErrorResponse handles and possible alters an error. This is a noop for +// the PeerRestrict rule. +// +// NOTE: this is part of the Enforcer interface. +func (c *PeerRestrictEnforcer) HandleErrorResponse(_ context.Context, + _ string, _ error) (error, error) { + + return nil, nil +} + +// checkers returns a map of URI to rpcmiddleware.RoundTripChecker which define +// how the URI should be handled. +func (c *PeerRestrictEnforcer) checkers() map[string]mid.RoundTripChecker { + return map[string]mid.RoundTripChecker{ + "/lnrpc.Lightning/UpdateChannelPolicy": mid.NewRequestChecker( + &lnrpc.PolicyUpdateRequest{}, + &lnrpc.PolicyUpdateResponse{}, + func(ctx context.Context, + r *lnrpc.PolicyUpdateRequest) error { + + if r.GetGlobal() { + return fmt.Errorf("cant apply call " + + "to global scope when using " + + "a peer restriction list") + } + + chanPoint := r.GetChanPoint() + if chanPoint == nil { + return fmt.Errorf("no channel point " + + "specified") + } + + txid, err := lnrpc.GetChanPointFundingTxid( + chanPoint, + ) + if err != nil { + return err + } + + index := chanPoint.GetOutputIndex() + point := fmt.Sprintf("%s:%d", txid, index) + + peerID, ok, err := c.mgr.getPeerFromChanPoint( + c.cfg, point, + ) + if err != nil { + return err + } else if !ok { + return nil + } + + if c.peerMap[peerID] { + return fmt.Errorf("illegal action on " + + "peer in peer restriction " + + "list") + } + + return nil + }, + ), + } +} + +// PeerRestrict is a rule prevents calls from acting upon a given set of peers. +type PeerRestrict struct { + // DenyList is a list of peer ids that should not be acted upon by any + // call. + DenyList []string `json:"peer_deny_list"` +} + +// VerifySane checks that the value of the values is ok given the min and max +// allowed values. This is a noop for the PeerRestrict rule. +// +// NOTE: this is part of the Values interface. +func (c *PeerRestrict) VerifySane(_, _ Values) error { + return nil +} + +// RuleName returns the name of the rule that these values are to be used with. +// +// NOTE: this is part of the Values interface. +func (c *PeerRestrict) RuleName() string { + return PeersRestrictName +} + +// ToProto converts the rule Values to the litrpc counterpart. +// +// NOTE: this is part of the Values interface. +func (c *PeerRestrict) ToProto() *litrpc.RuleValue { + return &litrpc.RuleValue{ + Value: &litrpc.RuleValue_PeerRestrict{ + PeerRestrict: &litrpc.PeerRestrict{ + PeerIds: c.DenyList, + }, + }, + } +} + +// PseudoToReal assumes that the deny-list contains pseudo peer IDs and uses +// these to check the privacy map db for the corresponding real peer IDs. +// It constructs a new PeerRestrict instance with these real peer IDs. +// +// NOTE: this is part of the Values interface. +func (c *PeerRestrict) PseudoToReal(db firewalldb.PrivacyMapDB) (Values, + error) { + + restrictList := make([]string, len(c.DenyList)) + err := db.View(func(tx firewalldb.PrivacyMapTx) error { + for i, chanID := range c.DenyList { + real, err := firewalldb.RevealString(tx, chanID) + if err != nil { + return err + } + + restrictList[i] = real + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return &PeerRestrict{ + DenyList: restrictList, + }, nil +} + +// RealToPseudo converts all the real peer IDs into pseudo IDs. +// +// NOTE: this is part of the Values interface. +func (c *PeerRestrict) RealToPseudo() (Values, map[string]string, error) { + pseudoIDs := make([]string, len(c.DenyList)) + privMapPairs := make(map[string]string) + for i, id := range c.DenyList { + // TODO(elle): check that this peer is actually one of our + // channel peers. + + if pseudo, ok := privMapPairs[id]; ok { + pseudoIDs[i] = pseudo + continue + } + + pseudo, err := firewalldb.NewPseudoStr(len(id)) + if err != nil { + return nil, nil, err + } + + privMapPairs[id] = pseudo + pseudoIDs[i] = pseudo + } + + return &PeerRestrict{DenyList: pseudoIDs}, privMapPairs, nil +} diff --git a/rules/peer_restrictions_test.go b/rules/peer_restrictions_test.go new file mode 100644 index 000000000..c14fc621c --- /dev/null +++ b/rules/peer_restrictions_test.go @@ -0,0 +1,152 @@ +package rules + +import ( + "context" + "encoding/hex" + "fmt" + "testing" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +// TestPeerRestrictCheckRequest ensures that the PeerRestrictEnforcer correctly +// accepts or denys a request. +func TestPeerRestrictCheckRequest(t *testing.T) { + txid1, index1, err := newTXID() + require.NoError(t, err) + + txid2, index2, err := newTXID() + require.NoError(t, err) + + txid3, index3, err := newTXID() + require.NoError(t, err) + + chanPointStr1 := fmt.Sprintf("%s:%d", hex.EncodeToString(txid1), index1) + chanPointStr2 := fmt.Sprintf("%s:%d", hex.EncodeToString(txid2), index2) + chanPointStr3 := fmt.Sprintf("%s:%d", hex.EncodeToString(txid3), index3) + + peerID1, err := firewalldb.NewPseudoStr(66) + require.NoError(t, err) + + peerID2, err := firewalldb.NewPseudoStr(66) + require.NoError(t, err) + + peerID3, err := firewalldb.NewPseudoStr(66) + require.NoError(t, err) + + peerKey1, err := route.NewVertexFromStr(peerID1) + require.NoError(t, err) + + peerKey2, err := route.NewVertexFromStr(peerID2) + require.NoError(t, err) + + peerKey3, err := route.NewVertexFromStr(peerID3) + require.NoError(t, err) + + ctx := context.Background() + mgr := NewPeerRestrictMgr() + cfg := &mockLndClient{ + channels: []lndclient.ChannelInfo{ + { + ChannelPoint: chanPointStr1, + PubKeyBytes: peerKey1, + }, + { + ChannelPoint: chanPointStr2, + PubKeyBytes: peerKey2, + }, + { + ChannelPoint: chanPointStr3, + PubKeyBytes: peerKey3, + }, + }, + } + + enf, err := mgr.NewEnforcer(cfg, &PeerRestrict{ + DenyList: []string{ + peerID1, peerID2, + }, + }) + require.NoError(t, err) + + // A request for an irrelevant URI should be allowed. + _, err = enf.HandleRequest(ctx, "random-URI", nil) + require.NoError(t, err) + + // If there is a channel restriction list, then no global policy updates + // are allowed. + _, err = enf.HandleRequest( + ctx, "/lnrpc.Lightning/UpdateChannelPolicy", + &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_Global{Global: true}, + }, + ) + require.ErrorContainsf(t, err, "cant apply call to global scope "+ + "when using a peer restriction list", "") + + // Test that an action on channel point 1 in the string form is + // disallowed. + chanPoint1 := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: hex.EncodeToString(txid1), + }, + OutputIndex: index1, + } + + _, err = enf.HandleRequest( + ctx, "/lnrpc.Lightning/UpdateChannelPolicy", + &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint1, + }, + }, + ) + require.ErrorContainsf(t, err, "illegal action on peer in peer "+ + "restriction list", "") + + // Test that an action on channel point 2 in the byte form is + // disallowed. + h, err := chainhash.NewHashFromStr(hex.EncodeToString(txid2)) + require.NoError(t, err) + + chanPoint2 := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ + FundingTxidBytes: h[:], + }, + OutputIndex: index2, + } + + _, err = enf.HandleRequest( + ctx, "/lnrpc.Lightning/UpdateChannelPolicy", + &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint2, + }, + }, + ) + require.ErrorContainsf(t, err, "illegal action on peer in peer "+ + "restriction list", "") + + // Test that an action on a channel not in the deny-list is allowed. + chanPoint3 := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: hex.EncodeToString(txid3), + }, + OutputIndex: index3, + } + + _, err = enf.HandleRequest( + ctx, "/lnrpc.Lightning/UpdateChannelPolicy", + &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint3, + }, + }, + ) + require.NoError(t, err) +} diff --git a/rules/rate_limit.go b/rules/rate_limit.go new file mode 100644 index 000000000..440f9b406 --- /dev/null +++ b/rules/rate_limit.go @@ -0,0 +1,282 @@ +package rules + +import ( + "context" + "fmt" + "time" + + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lightning-terminal/litrpc" + "google.golang.org/protobuf/proto" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +var ( + // Compile-time checks to ensure that RateLimit, RateLimitMgr + // and RateLimitEnforcer implement the appropriate Manager, Enforcer + // and Values interface. + _ Manager = (*RateLimitMgr)(nil) + _ Enforcer = (*RateLimitEnforcer)(nil) + _ Values = (*RateLimit)(nil) +) + +// RateLimitName is the string identifier of the RateLimitMgr values. +const RateLimitName = "rate-limit" + +// RateLimitMgr represents the rate limit values. +type RateLimitMgr struct{} + +// Stop cleans up the resources held by the manager. +// +// NOTE: This is part of the Manager interface. +func (r *RateLimitMgr) Stop() error { + return nil +} + +// NewEnforcer constructs a new RateLimit rule enforcer using the passed values +// and config. +// +// NOTE: This is part of the Manager interface. +func (r *RateLimitMgr) NewEnforcer(cfg Config, values Values) (Enforcer, + error) { + + limits, ok := values.(*RateLimit) + if !ok { + return nil, fmt.Errorf("values must be of type "+ + "RateLimit, got %T", values) + } + + return &RateLimitEnforcer{ + rateLimitConfig: cfg, + RateLimit: limits, + }, nil +} + +// NewValueFromProto converts the given proto value into a RateLimit Value +// object. +// +// NOTE: This is part of the Manager interface. +func (r *RateLimitMgr) NewValueFromProto(v *litrpc.RuleValue) (Values, error) { + rv, ok := v.Value.(*litrpc.RuleValue_RateLimit) + if !ok { + return nil, fmt.Errorf("incorrect RuleValue type") + } + + budget := rv.RateLimit + readLim := budget.ReadLimit + writeLim := budget.WriteLimit + + return &RateLimit{ + ReadLimit: &Rate{ + Iterations: readLim.Iterations, + NumHours: readLim.NumHours, + }, + WriteLimit: &Rate{ + Iterations: writeLim.Iterations, + NumHours: writeLim.NumHours, + }, + }, nil +} + +// EmptyValue returns a new RateLimit instance. +func (r *RateLimitMgr) EmptyValue() Values { + return &RateLimit{} +} + +// rateLimitConfig is the config required by RateLimitMgr. It can be derived +// from the main rules Config struct. +type rateLimitConfig interface { + GetActionsDB() firewalldb.ActionsDB + GetMethodPerms() func(string) ([]bakery.Op, bool) +} + +// RateLimitEnforcer enforces requests and responses against a RateLimit rule. +type RateLimitEnforcer struct { + rateLimitConfig + *RateLimit +} + +// HandleResponse handles and possible alters a response. This is a noop for the +// RateLimitMgr values. +// +// NOTE: this is part of the Rule interface. +func (r *RateLimitEnforcer) HandleResponse(_ context.Context, _ string, + _ proto.Message) (proto.Message, error) { + + return nil, nil +} + +// HandleRequest checks the validity of a request. It checks if the request is a +// read or a write request. Then, using the past actions DB, it determines if +// letting this request through would violate the rate limit rules. +// +// NOTE: this is part of the Rule interface. +func (r *RateLimitEnforcer) HandleRequest(ctx context.Context, uri string, + _ proto.Message) (proto.Message, error) { + + // First, we need to classify if this is a read or write call. + read := r.isRead(uri) + + // Based on the above, we can extract the relevant rate limit values + // that apply for this call. + rateLim := r.WriteLimit + if read { + rateLim = r.ReadLimit + } + + // Now we need to go and count all the previous read or write actions. + actions, err := r.GetActionsDB().ListActions(ctx) + if err != nil { + return nil, err + } + + // Determine the start time of the actions window. + startTime := time.Now().Add( + -time.Duration(rateLim.NumHours) * time.Hour, + ) + + // Now count all relevant actions which have taken place after the + // start time. + var count uint32 + for _, action := range actions { + if read != r.isRead(action.Method) { + continue + } + + if action.PerformedAt.Before(startTime) { + continue + } + + count++ + } + + if count >= rateLim.Iterations { + return nil, fmt.Errorf("too many requests received") + } + + return nil, nil +} + +// HandleErrorResponse handles and possible alters an error. This is a noop for +// the RateLimitEnforcer rule. +// +// NOTE: this is part of the Enforcer interface. +func (r *RateLimitEnforcer) HandleErrorResponse(_ context.Context, _ string, + _ error) (error, error) { + + return nil, nil +} + +// isRead is a helper that returns true if the given method/URI only requires +// read-permissions and false otherwise. +func (r *RateLimitEnforcer) isRead(method string) bool { + perms, ok := r.GetMethodPerms()(method) + if !ok { + return false + } + + for _, p := range perms { + if p.Action != "read" { + return false + } + } + return true +} + +// Rate describes a rate limit in iterations per number of hours. +type Rate struct { + Iterations uint32 `json:"iterations"` + NumHours uint32 `json:"num_hours"` +} + +// RateLimit represents the rules values. +type RateLimit struct { + WriteLimit *Rate `json:"write_limit"` + ReadLimit *Rate `json:"read_limit"` +} + +// VerifySane checks that the value of the values is ok given the min and max +// allowed values. +// +// NOTE: this is part of the Values interface. +func (r *RateLimit) VerifySane(minVal, maxVal Values) error { + minRL, ok := minVal.(*RateLimit) + if !ok { + return fmt.Errorf("min value is not of type RateLimit") + } + + maxRL, ok := maxVal.(*RateLimit) + if !ok { + return fmt.Errorf("max value is not of type RateLimit") + } + + // Check that our read limit is between the min and max. + if r.ReadLimit.lessThan(minRL.ReadLimit) || + maxRL.ReadLimit.lessThan(r.ReadLimit) { + + return fmt.Errorf("read limit is not between the min and max") + } + + // Check that our write limit is between the min and max. + if r.WriteLimit.lessThan(minRL.WriteLimit) || + maxRL.WriteLimit.lessThan(r.WriteLimit) { + + return fmt.Errorf("write limit is not between the min and max") + } + + return nil +} + +// lessThan is a helper function that checks if the current rate is less than +// another rate. +func (r *Rate) lessThan(other *Rate) bool { + return float64(r.Iterations)/float64(r.NumHours) < + float64(other.Iterations)/float64(other.NumHours) +} + +// RuleName returns the name of the rule that these values are to be used with. +// +// NOTE: this is part of the Values interface. +func (r *RateLimit) RuleName() string { + return RateLimitName +} + +// ToProto converts the rule Values to the litrpc counterpart. +// +// NOTE: this is part of the Values interface. +func (r *RateLimit) ToProto() *litrpc.RuleValue { + return &litrpc.RuleValue{ + Value: &litrpc.RuleValue_RateLimit{ + RateLimit: &litrpc.RateLimit{ + ReadLimit: &litrpc.Rate{ + Iterations: r.ReadLimit.Iterations, + NumHours: r.ReadLimit.NumHours, + }, + WriteLimit: &litrpc.Rate{ + Iterations: r.WriteLimit.Iterations, + NumHours: r.WriteLimit.NumHours, + }, + }, + }, + } +} + +// PseudoToReal attempts to convert any appropriate pseudo fields in the rule +// Values to their corresponding real values. It uses the passed PrivacyMapDB to +// find the real values. This is a no-op for the RateLimit rule. +// +// NOTE: this is part of the Values interface. +func (r *RateLimit) PseudoToReal(_ firewalldb.PrivacyMapDB) (Values, + error) { + + return r, nil +} + +// RealToPseudo converts the rule Values to a new one that uses pseudo keys, +// channel IDs, channel points etc. It returns a map of real to pseudo strings +// that should be persisted. This is a no-op for the RateLimit rule. +// +// NOTE: this is part of the Values interface. +func (r *RateLimit) RealToPseudo() (Values, map[string]string, error) { + return r, nil, nil +} diff --git a/rules/rate_limit_test.go b/rules/rate_limit_test.go new file mode 100644 index 000000000..257232b65 --- /dev/null +++ b/rules/rate_limit_test.go @@ -0,0 +1,249 @@ +package rules + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/stretchr/testify/require" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +// TestRateLimitVerifySane tests that the RateLimit VerifySane method +// correctly verifies the value of the rate limit depending on given min and +// max sane values. +func TestRateLimitVerifySane(t *testing.T) { + var ( + min = &RateLimit{ + WriteLimit: &Rate{ + Iterations: 1, + NumHours: 24 * 7, + }, + ReadLimit: &Rate{ + Iterations: 1, + NumHours: 24 * 7, + }, + } + max = &RateLimit{ + WriteLimit: &Rate{ + Iterations: 1, + NumHours: 24, + }, + ReadLimit: &Rate{ + Iterations: 5, + NumHours: 1, + }, + } + ) + + tests := []struct { + name string + values *RateLimit + expectErr error + }{ + { + name: "between bounds", + values: &RateLimit{ + WriteLimit: &Rate{ + Iterations: 1, + NumHours: 48, + }, + ReadLimit: &Rate{ + Iterations: 2, + NumHours: 1, + }, + }, + }, + { + name: "read limit below bounds", + values: &RateLimit{ + WriteLimit: &Rate{ + Iterations: 1, + NumHours: 48, + }, + ReadLimit: &Rate{ + Iterations: 1, + NumHours: 24 * 14, + }, + }, + expectErr: fmt.Errorf("read limit is not between " + + "the min and max"), + }, + { + name: "read limit above bounds", + values: &RateLimit{ + WriteLimit: &Rate{ + Iterations: 1, + NumHours: 48, + }, + ReadLimit: &Rate{ + Iterations: 100, + NumHours: 1, + }, + }, + expectErr: fmt.Errorf("read limit is not between " + + "the min and max"), + }, + { + name: "write limit below bounds", + values: &RateLimit{ + WriteLimit: &Rate{ + Iterations: 1, + NumHours: 24 * 14, + }, + ReadLimit: &Rate{ + Iterations: 1, + NumHours: 24 * 7, + }, + }, + expectErr: fmt.Errorf("write limit is not between " + + "the min and max"), + }, + { + name: "write limit above bounds", + values: &RateLimit{ + WriteLimit: &Rate{ + Iterations: 10, + NumHours: 24, + }, + ReadLimit: &Rate{ + Iterations: 1, + NumHours: 24 * 7, + }, + }, + expectErr: fmt.Errorf("write limit is not between " + + "the min and max"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.values.VerifySane(min, max) + require.Equal(t, test.expectErr, err) + }) + } +} + +// TestRateLimitCheckRequest checks that a request is correctly accepted or +// denied based on the RateLimitMgr values values. +func TestRateLimitCheckRequest(t *testing.T) { + ctx := context.Background() + + // Create a new Actions DB. + db := &mockActionsDB{} + + // Define a mock permissions map with a few read and write URIs. + perms := map[string][]bakery.Op{ + "read-uri": {{Action: "read"}}, + "write-uri": {{Action: "write"}}, + "read-write-uri": {{Action: "write"}, {Action: "read"}}, + } + + // Create a new config struct. + cfg := &mockRateLimitCfg{ + db: db, + perms: perms, + } + + // Initialise the new values. + values := &RateLimit{ + WriteLimit: &Rate{ + Iterations: 1, + NumHours: 24, + }, + ReadLimit: &Rate{ + Iterations: 2, + NumHours: 1, + }, + } + + enf := &RateLimitEnforcer{ + rateLimitConfig: cfg, + RateLimit: values, + } + + // The actions DB is currently empty. So this request should go through. + _, err := enf.HandleRequest(ctx, "write-uri", nil) + require.NoError(t, err) + + // Add a write action to the DB that took place long ago. + db.addAction("write-uri", time.Now().Add(-25*time.Hour)) + + // Since the above action took place more than 24 hours ago and the rate + // limit values defines the write-limit as 1 per 24 hours, a write call + // should still be allowed. + _, err = enf.HandleRequest(ctx, "write-uri", nil) + require.NoError(t, err) + + // Now we add a more recent write action to the DB. + db.addAction("write-uri", time.Now()) + + // Since the rate limit values only allows one write action per 24 hours, + // a request for another write action should not be allowed. + _, err = enf.HandleRequest(ctx, "write-uri", nil) + require.Error(t, err) + + // A read request should still be allowed since we have not exceeded + // the read limit yet. + _, err = enf.HandleRequest(ctx, "read-uri", nil) + require.NoError(t, err) + + // Add one read action to the db. + db.addAction("read-uri", time.Now()) + + // Since the limit is 2 read actions per hour, we should still be able + // to make another read call. + _, err = enf.HandleRequest(ctx, "read-uri", nil) + require.NoError(t, err) + + // Add one more read action to the db. + db.addAction("read-uri", time.Now()) + + // Another read call should now exceed the limit and so should not be + // allowed. + _, err = enf.HandleRequest(ctx, "read-uri", nil) + require.Error(t, err) +} + +// mockRateLimitCfg is used to mock the config backend given to the RateLimitMgr +// values during testing. +type mockRateLimitCfg struct { + db *mockActionsDB + perms map[string][]bakery.Op +} + +var _ rateLimitConfig = (*mockRateLimitCfg)(nil) + +func (m *mockRateLimitCfg) GetActionsDB() firewalldb.ActionsDB { + return m.db +} + +func (m *mockRateLimitCfg) GetMethodPerms() func(string) ([]bakery.Op, bool) { + return func(s string) ([]bakery.Op, bool) { + ops, ok := m.perms[s] + return ops, ok + } +} + +// mockActionsDB is used to mock the action's db backend used by the RateLimitMgr +// values. +type mockActionsDB struct { + actions []*firewalldb.RuleAction +} + +var _ firewalldb.ActionsDB = (*mockActionsDB)(nil) + +func (m *mockActionsDB) addAction(uri string, timestamp time.Time) { + m.actions = append(m.actions, &firewalldb.RuleAction{ + Method: uri, + PerformedAt: timestamp, + }) +} + +func (m *mockActionsDB) ListActions(_ context.Context) ( + []*firewalldb.RuleAction, error) { + + return m.actions, nil +} diff --git a/session/interface.go b/session/interface.go index 30f55a612..a87d64f80 100644 --- a/session/interface.go +++ b/session/interface.go @@ -15,12 +15,12 @@ import ( type Type uint8 const ( - TypeMacaroonReadonly Type = 0 - TypeMacaroonAdmin Type = 1 - TypeMacaroonCustom Type = 2 - TypeUIPassword Type = 3 - typeReservedForFutureUse Type = 4 - TypeMacaroonAccount Type = 5 + TypeMacaroonReadonly Type = 0 + TypeMacaroonAdmin Type = 1 + TypeMacaroonCustom Type = 2 + TypeUIPassword Type = 3 + TypeAutopilot Type = 4 + TypeMacaroonAccount Type = 5 ) // State represents the state of a session. @@ -40,22 +40,29 @@ type MacaroonRecipe struct { Caveats []macaroon.Caveat } +// FeaturesConfig is a map from feature name to a raw byte array which stores +// any config feature config options. +type FeaturesConfig map[string][]byte + // Session is a struct representing a long-term Terminal Connect session. type Session struct { - Label string - State State - Type Type - Expiry time.Time - CreatedAt time.Time - RevokedAt time.Time - ServerAddr string - DevServer bool - MacaroonRootKey uint64 - MacaroonRecipe *MacaroonRecipe - PairingSecret [mailbox.NumPassphraseEntropyBytes]byte - LocalPrivateKey *btcec.PrivateKey - LocalPublicKey *btcec.PublicKey - RemotePublicKey *btcec.PublicKey + ID ID + Label string + State State + Type Type + Expiry time.Time + CreatedAt time.Time + RevokedAt time.Time + ServerAddr string + DevServer bool + MacaroonRootKey uint64 + MacaroonRecipe *MacaroonRecipe + PairingSecret [mailbox.NumPassphraseEntropyBytes]byte + LocalPrivateKey *btcec.PrivateKey + LocalPublicKey *btcec.PublicKey + RemotePublicKey *btcec.PublicKey + FeatureConfig *FeaturesConfig + WithPrivacyMapper bool } // MacaroonBaker is a function type for baking a super macaroon. @@ -64,8 +71,8 @@ type MacaroonBaker func(ctx context.Context, rootKeyID uint64, // NewSession creates a new session with the given user-defined parameters. func NewSession(label string, typ Type, expiry time.Time, serverAddr string, - devServer bool, perms []bakery.Op, caveats []macaroon.Caveat) (*Session, - error) { + devServer bool, perms []bakery.Op, caveats []macaroon.Caveat, + featureConfig FeaturesConfig, privacy bool) (*Session, error) { _, pairingSecret, err := mailbox.NewPassphraseEntropy() if err != nil { @@ -83,18 +90,20 @@ func NewSession(label string, typ Type, expiry time.Time, serverAddr string, macRootKey := NewSuperMacaroonRootKeyID(macRootKeyBase) sess := &Session{ - Label: label, - State: StateCreated, - Type: typ, - Expiry: expiry, - CreatedAt: time.Now(), - ServerAddr: serverAddr, - DevServer: devServer, - MacaroonRootKey: macRootKey, - PairingSecret: pairingSecret, - LocalPrivateKey: privateKey, - LocalPublicKey: pubKey, - RemotePublicKey: nil, + ID: macRootKeyBase, + Label: label, + State: StateCreated, + Type: typ, + Expiry: expiry, + CreatedAt: time.Now(), + ServerAddr: serverAddr, + DevServer: devServer, + MacaroonRootKey: macRootKey, + PairingSecret: pairingSecret, + LocalPrivateKey: privateKey, + LocalPublicKey: pubKey, + RemotePublicKey: nil, + WithPrivacyMapper: privacy, } if perms != nil || caveats != nil { @@ -104,6 +113,10 @@ func NewSession(label string, typ Type, expiry time.Time, serverAddr string, } } + if len(featureConfig) != 0 { + sess.FeatureConfig = &featureConfig + } + return sess, nil } diff --git a/session/macaroon.go b/session/macaroon.go index 55b56d1e4..48a1e07b3 100644 --- a/session/macaroon.go +++ b/session/macaroon.go @@ -5,6 +5,7 @@ import ( "context" "encoding/binary" "encoding/hex" + "fmt" "strconv" "github.com/lightningnetwork/lnd/lnrpc" @@ -19,6 +20,9 @@ var ( SuperMacaroonRootKeyPrefix = [4]byte{0xFF, 0xEE, 0xDD, 0xCC} ) +// ID represents the id of a session. +type ID [4]byte + // SuperMacaroonValidator is a function type for validating a super macaroon. type SuperMacaroonValidator func(ctx context.Context, superMacaroon []byte, requiredPermissions []bakery.Op, @@ -56,19 +60,7 @@ func IsSuperMacaroon(macHex string) bool { return false } - rawID := mac.Id() - if rawID[0] != byte(bakery.LatestVersion) { - return false - } - decodedID := &lnrpc.MacaroonId{} - idProto := rawID[1:] - err = proto.Unmarshal(idProto, decodedID) - if err != nil { - return false - } - - // The storage ID is a string representation of a 64bit unsigned number. - rootKeyID, err := strconv.ParseUint(string(decodedID.StorageId), 10, 64) + rootKeyID, err := RootKeyIDFromMacaroon(mac) if err != nil { return false } @@ -84,3 +76,54 @@ func isSuperMacaroonRootKeyID(rootKeyID uint64) bool { binary.BigEndian.PutUint64(rootKeyBytes, rootKeyID) return bytes.HasPrefix(rootKeyBytes, SuperMacaroonRootKeyPrefix[:]) } + +// IDFromMacaroon is a helper function that creates a session ID from +// a macaroon ID. +func IDFromMacaroon(mac *macaroon.Macaroon) (ID, error) { + rootKeyID, err := RootKeyIDFromMacaroon(mac) + if err != nil { + return ID{}, err + } + + return IDFromMacRootKeyID(rootKeyID), nil +} + +// IDFromMacRootKeyID converts a macaroon root key ID to a session ID. +func IDFromMacRootKeyID(rootKeyID uint64) ID { + rootKeyBytes := make([]byte, 8) + binary.BigEndian.PutUint64(rootKeyBytes[:], rootKeyID) + + var id ID + copy(id[:], rootKeyBytes[4:]) + + return id +} + +// IDFromBytes is a helper function that creates a session ID from a byte slice. +func IDFromBytes(b []byte) (ID, error) { + var id ID + if len(b) != 4 { + return id, fmt.Errorf("session ID must be 4 bytes long") + } + copy(id[:], b) + return id, nil +} + +// RootKeyIDFromMacaroon extracts the root key ID of the passed macaroon. +func RootKeyIDFromMacaroon(mac *macaroon.Macaroon) (uint64, error) { + rawID := mac.Id() + if rawID[0] != byte(bakery.LatestVersion) { + return 0, fmt.Errorf("mac id is not on the latest version") + } + + decodedID := &lnrpc.MacaroonId{} + idProto := rawID[1:] + err := proto.Unmarshal(idProto, decodedID) + if err != nil { + return 0, err + } + + // The storage ID is a string representation of a 64-bit unsigned + // number. + return strconv.ParseUint(string(decodedID.StorageId), 10, 64) +} diff --git a/session/store.go b/session/store.go index 049012c39..82abe2e27 100644 --- a/session/store.go +++ b/session/store.go @@ -45,8 +45,36 @@ func (db *DB) StoreSession(session *Session) error { }) } +// GetSession fetches the session with the given key. +func (db *DB) GetSession(key *btcec.PublicKey) (*Session, error) { + var session *Session + err := db.View(func(tx *bbolt.Tx) error { + sessionBucket, err := getBucket(tx, sessionBucketKey) + if err != nil { + return err + } + + v := sessionBucket.Get(key.SerializeCompressed()) + if len(v) == 0 { + return ErrSessionNotFound + } + + session, err = DeserializeSession(bytes.NewReader(v)) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + return session, nil +} + // ListSessions returns all sessions currently known to the store. -func (db *DB) ListSessions() ([]*Session, error) { +func (db *DB) ListSessions(filterFn func(s *Session) bool) ([]*Session, error) { var sessions []*Session err := db.View(func(tx *bbolt.Tx) error { sessionBucket, err := getBucket(tx, sessionBucketKey) @@ -65,12 +93,16 @@ func (db *DB) ListSessions() ([]*Session, error) { if err != nil { return err } + + if filterFn != nil && !filterFn(session) { + return nil + } + sessions = append(sessions, session) return nil }) }) - if err != nil { return nil, err } diff --git a/session/tlv.go b/session/tlv.go index 07d916f2e..f2dbb88c8 100644 --- a/session/tlv.go +++ b/session/tlv.go @@ -25,8 +25,8 @@ const ( typeRemotePublicKey tlv.Type = 11 typeMacaroonRecipe tlv.Type = 12 typeCreatedAt tlv.Type = 13 - typeReservedNum1 tlv.Type = 14 - typeReservedNum2 tlv.Type = 15 + typeFeaturesConfig tlv.Type = 14 + typeWithPrivacy tlv.Type = 15 typeRevokedAt tlv.Type = 16 // typeMacaroon is no longer used, but we leave it defined for backwards @@ -42,6 +42,9 @@ const ( typePermEntity tlv.Type = 1 typePermAction tlv.Type = 2 + + typeFeatureName tlv.Type = 1 + typeFeatureConfig tlv.Type = 2 ) // SerializeSession binary serializes the given session to the writer using the @@ -62,12 +65,17 @@ func SerializeSession(w io.Writer, session *Session) error { privateKey = session.LocalPrivateKey.Serialize() createdAt = uint64(session.CreatedAt.Unix()) revokedAt uint64 + withPrivacy = uint8(0) ) if !session.RevokedAt.IsZero() { revokedAt = uint64(session.RevokedAt.Unix()) } + if session.WithPrivacyMapper { + withPrivacy = 1 + } + if session.DevServer { devServer = 1 } @@ -111,6 +119,24 @@ func SerializeSession(w io.Writer, session *Session) error { tlvRecords = append( tlvRecords, tlv.MakePrimitiveRecord(typeCreatedAt, &createdAt), + ) + + if session.FeatureConfig != nil && len(*session.FeatureConfig) != 0 { + tlvRecords = append(tlvRecords, tlv.MakeDynamicRecord( + typeFeaturesConfig, session.FeatureConfig, + func() uint64 { + return recordSize( + featureConfigEncoder, + session.FeatureConfig, + ) + }, + featureConfigEncoder, featureConfigDecoder, + )) + } + + tlvRecords = append( + tlvRecords, + tlv.MakePrimitiveRecord(typeWithPrivacy, &withPrivacy), tlv.MakePrimitiveRecord(typeRevokedAt, &revokedAt), ) @@ -126,12 +152,13 @@ func SerializeSession(w io.Writer, session *Session) error { // the data to be encoded in the tlv format. func DeserializeSession(r io.Reader) (*Session, error) { var ( - session = &Session{} - label, serverAddr []byte - pairingSecret, privateKey []byte - state, typ, devServer uint8 - expiry, createdAt, revokedAt uint64 - macRecipe MacaroonRecipe + session = &Session{} + label, serverAddr []byte + pairingSecret, privateKey []byte + state, typ, devServer, privacy uint8 + expiry, createdAt, revokedAt uint64 + macRecipe MacaroonRecipe + featureConfig FeaturesConfig ) tlvStream, err := tlv.NewStream( tlv.MakePrimitiveRecord(typeLabel, &label), @@ -153,6 +180,11 @@ func DeserializeSession(r io.Reader) (*Session, error) { macaroonRecipeEncoder, macaroonRecipeDecoder, ), tlv.MakePrimitiveRecord(typeCreatedAt, &createdAt), + tlv.MakeDynamicRecord( + typeFeaturesConfig, &featureConfig, nil, + featureConfigEncoder, featureConfigDecoder, + ), + tlv.MakePrimitiveRecord(typeWithPrivacy, &privacy), tlv.MakePrimitiveRecord(typeRevokedAt, &revokedAt), ) if err != nil { @@ -164,6 +196,7 @@ func DeserializeSession(r io.Reader) (*Session, error) { return nil, err } + session.ID = IDFromMacRootKeyID(session.MacaroonRootKey) session.Label = string(label) session.State = State(state) session.Type = Type(typ) @@ -171,6 +204,7 @@ func DeserializeSession(r io.Reader) (*Session, error) { session.CreatedAt = time.Unix(int64(createdAt), 0) session.ServerAddr = string(serverAddr) session.DevServer = devServer == 1 + session.WithPrivacyMapper = privacy == 1 if revokedAt != 0 { session.RevokedAt = time.Unix(int64(revokedAt), 0) @@ -190,9 +224,115 @@ func DeserializeSession(r io.Reader) (*Session, error) { ) } + if t, ok := parsedTypes[typeFeaturesConfig]; ok && t == nil { + session.FeatureConfig = &featureConfig + } + return session, nil } +func featureConfigEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*FeaturesConfig); ok { + for n, config := range *v { + name := []byte(n) + config := config + + var permsTLVBytes bytes.Buffer + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord(typeFeatureName, &name), + tlv.MakePrimitiveRecord( + typeFeatureConfig, &config, + ), + ) + if err != nil { + return err + } + + err = tlvStream.Encode(&permsTLVBytes) + if err != nil { + return err + } + + // We encode the record with a varint length followed by + // the _raw_ TLV bytes. + tlvLen := uint64(len(permsTLVBytes.Bytes())) + if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil { + return err + } + + _, err = w.Write(permsTLVBytes.Bytes()) + if err != nil { + return err + } + } + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "FeaturesConfig") +} + +func featureConfigDecoder(r io.Reader, val interface{}, buf *[8]byte, + l uint64) error { + + if v, ok := val.(*FeaturesConfig); ok { + featureConfig := make(map[string][]byte) + + // Using this information, we'll create a new limited + // reader that'll return an EOF once the end has been + // reached so the stream stops consuming bytes. + innerTlvReader := io.LimitedReader{ + R: r, + N: int64(l), + } + + for { + // Read out the varint that encodes the size of this + // inner TLV record. + blobSize, err := tlv.ReadVarInt(&innerTlvReader, buf) + if err == io.EOF { + break + } else if err != nil { + return err + } + + innerInnerTlvReader := io.LimitedReader{ + R: &innerTlvReader, + N: int64(blobSize), + } + + var ( + name []byte + config []byte + ) + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord( + typeFeatureName, &name, + ), + tlv.MakePrimitiveRecord( + typeFeatureConfig, &config, + ), + ) + if err != nil { + return err + } + + err = tlvStream.Decode(&innerInnerTlvReader) + if err != nil { + return err + } + + featureConfig[string(name)] = config + } + + *v = featureConfig + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "FeaturesConfig") +} + // macaroonRecipeEncoder is a custom TLV encoder for a MacaroonRecipe record. func macaroonRecipeEncoder(w io.Writer, val interface{}, buf *[8]byte) error { if v, ok := val.(*MacaroonRecipe); ok { diff --git a/session/tlv_test.go b/session/tlv_test.go index 2809f4bb9..f1d211dd8 100644 --- a/session/tlv_test.go +++ b/session/tlv_test.go @@ -51,11 +51,12 @@ var ( // and deserialized from and to the tlv binary format successfully. func TestSerializeDeserializeSession(t *testing.T) { tests := []struct { - name string - sessType Type - revokedAt time.Time - perms []bakery.Op - caveats []macaroon.Caveat + name string + sessType Type + revokedAt time.Time + perms []bakery.Op + caveats []macaroon.Caveat + featureConfig map[string][]byte }{ { name: "session 1", @@ -70,6 +71,14 @@ func TestSerializeDeserializeSession(t *testing.T) { perms: perms, caveats: caveats, }, + { + name: "session 3", + sessType: TypeMacaroonCustom, + featureConfig: map[string][]byte{ + "AutoFees": {1, 2, 3, 4}, + "AutoSomething": {4, 3, 4, 5, 6, 6}, + }, + }, } for _, test := range tests { @@ -78,7 +87,7 @@ func TestSerializeDeserializeSession(t *testing.T) { test.name, test.sessType, time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC), "foo.bar.baz:1234", true, test.perms, - test.caveats, + test.caveats, test.featureConfig, true, ) require.NoError(t, err) diff --git a/session_rpcserver.go b/session_rpcserver.go index c8439479f..740cbddf9 100644 --- a/session_rpcserver.go +++ b/session_rpcserver.go @@ -2,6 +2,8 @@ package terminal import ( "context" + "encoding/json" + "errors" "fmt" "strings" "sync" @@ -10,8 +12,12 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/lightninglabs/lightning-node-connect/mailbox" "github.com/lightninglabs/lightning-terminal/accounts" + "github.com/lightninglabs/lightning-terminal/autopilotserver" + "github.com/lightninglabs/lightning-terminal/firewall" + "github.com/lightninglabs/lightning-terminal/firewalldb" "github.com/lightninglabs/lightning-terminal/litrpc" "github.com/lightninglabs/lightning-terminal/perms" + "github.com/lightninglabs/lightning-terminal/rules" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" @@ -31,6 +37,8 @@ const readOnlyAction = "***readonly***" // sessionRpcServer is the gRPC server for the Session RPC interface. type sessionRpcServer struct { litrpc.UnimplementedSessionsServer + litrpc.UnimplementedFirewallServer + litrpc.UnimplementedAutopilotServer cfg *sessionRpcServerConfig db *session.DB @@ -51,6 +59,10 @@ type sessionRpcServerConfig struct { superMacBaker session.MacaroonBaker firstConnectionDeadline time.Duration permMgr *perms.Manager + actionsDB *firewalldb.DB + autopilot autopilotserver.Autopilot + ruleMgrs rules.ManagerSet + privMap firewalldb.NewPrivacyMapDB } // newSessionRPCServer creates a new sessionRpcServer using the passed config. @@ -86,17 +98,75 @@ func newSessionRPCServer(cfg *sessionRpcServerConfig) (*sessionRpcServer, } // start all the components necessary for the sessionRpcServer to start serving -// requests. This includes starting the macaroon service and resuming all -// non-revoked sessions. +// requests. This includes resuming all non-revoked sessions. func (s *sessionRpcServer) start() error { // Start up all previously created sessions. - sessions, err := s.db.ListSessions() + sessions, err := s.db.ListSessions(nil) if err != nil { return fmt.Errorf("error listing sessions: %v", err) } + for _, sess := range sessions { + key := sess.LocalPublicKey.SerializeCompressed() + + if sess.Type == session.TypeAutopilot { + // We only start the autopilot sessions if the autopilot + // client has been enabled. + if s.cfg.autopilot == nil { + continue + } + + // Do a sanity check to ensure that we have the static + // remote pub key stored for this session. This should + // never not be the case. + if sess.RemotePublicKey == nil { + log.Errorf("no static remote key found for "+ + "autopilot session %x", key) + + continue + } + + if sess.State != session.StateInUse && + sess.State != session.StateCreated { + + continue + } + + if sess.Expiry.Before(time.Now()) { + continue + } + + ctx := context.Background() + ctxc, cancel := context.WithTimeout( + ctx, defaultConnectTimeout, + ) + + // Register the session with the autopilot client. + perm, err := s.cfg.autopilot.ActivateSession( + ctxc, sess.LocalPublicKey, + ) + cancel() + if err != nil { + log.Errorf("error activating autopilot "+ + "session (%x) with the client", key, + err) + + if perm { + err := s.db.RevokeSession( + sess.LocalPublicKey, + ) + if err != nil { + log.Errorf("error revoking "+ + "session: %v", err) + } + + continue + } + } + } + if err := s.resumeSession(sess); err != nil { - return fmt.Errorf("error resuming sesion: %v", err) + log.Errorf("error resuming session (%x): %v", key, err) } } @@ -229,8 +299,9 @@ func (s *sessionRpcServer) AddSession(_ context.Context, // No other types are currently supported. default: return nil, fmt.Errorf("invalid session type, only admin, " + - "readonly, account and custom macaroon types " + - "supported in LiT") + "readonly, custom and account macaroon types supported in " + + "LiT. Autopilot sessions must be added using " + + "AddAutoPilotSession method") } // Collect the de-duped permissions. @@ -246,7 +317,7 @@ func (s *sessionRpcServer) AddSession(_ context.Context, sess, err := session.NewSession( req.Label, typ, expiry, req.MailboxServerAddr, req.DevServer, - uniquePermissions, caveats, + uniquePermissions, caveats, nil, false, ) if err != nil { return nil, fmt.Errorf("error creating new session: %v", err) @@ -260,7 +331,7 @@ func (s *sessionRpcServer) AddSession(_ context.Context, return nil, fmt.Errorf("error starting session: %v", err) } - rpcSession, err := marshalRPCSession(sess) + rpcSession, err := s.marshalRPCSession(sess) if err != nil { return nil, fmt.Errorf("error marshaling session: %v", err) } @@ -322,11 +393,14 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error { // For custom session types, we use the caveats and permissions that // were persisted on session creation. - case session.TypeMacaroonCustom: - if sess.MacaroonRecipe != nil { - permissions = sess.MacaroonRecipe.Permissions + case session.TypeMacaroonCustom, session.TypeAutopilot: + if sess.MacaroonRecipe == nil { + break } + permissions = sess.MacaroonRecipe.Permissions + caveats = append(caveats, sess.MacaroonRecipe.Caveats...) + // No other types are currently supported. default: log.Debugf("Not resuming session %x with type %d", pubKeyBytes, @@ -433,16 +507,24 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error { pubKeyBytes) } + if s.cfg.autopilot != nil { + ctx := context.Background() + ctxc, cancel := context.WithTimeout( + ctx, defaultConnectTimeout, + ) + + s.cfg.autopilot.SessionRevoked(ctxc, pubKey) + cancel() + } + err = s.sessionServer.StopSession(pubKey) if err != nil { - log.Debugf("Error stopping session: "+ - "%v", err) + log.Debugf("Error stopping session: %v", err) } err = s.db.RevokeSession(pubKey) if err != nil { - log.Debugf("error revoking session: "+ - "%v", err) + log.Debugf("error revoking session: %v", err) } }() @@ -453,7 +535,7 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error { func (s *sessionRpcServer) ListSessions(_ context.Context, _ *litrpc.ListSessionsRequest) (*litrpc.ListSessionsResponse, error) { - sessions, err := s.db.ListSessions() + sessions, err := s.db.ListSessions(nil) if err != nil { return nil, fmt.Errorf("error fetching sessions: %v", err) } @@ -462,7 +544,7 @@ func (s *sessionRpcServer) ListSessions(_ context.Context, Sessions: make([]*litrpc.Session, len(sessions)), } for idx, sess := range sessions { - response.Sessions[idx], err = marshalRPCSession(sess) + response.Sessions[idx], err = s.marshalRPCSession(sess) if err != nil { return nil, fmt.Errorf("error marshaling session: %v", err) @@ -474,7 +556,7 @@ func (s *sessionRpcServer) ListSessions(_ context.Context, // RevokeSession revokes a single session and also stops it if it is currently // active. -func (s *sessionRpcServer) RevokeSession(_ context.Context, +func (s *sessionRpcServer) RevokeSession(ctx context.Context, req *litrpc.RevokeSessionRequest) (*litrpc.RevokeSessionResponse, error) { pubKey, err := btcec.ParsePubKey(req.LocalPublicKey) @@ -486,6 +568,10 @@ func (s *sessionRpcServer) RevokeSession(_ context.Context, return nil, fmt.Errorf("error revoking session: %v", err) } + if s.cfg.autopilot != nil { + s.cfg.autopilot.SessionRevoked(ctx, pubKey) + } + // If the session expired already it might not be running anymore. So we // only log possible errors here. if err := s.sessionServer.StopSession(pubKey); err != nil { @@ -495,8 +581,593 @@ func (s *sessionRpcServer) RevokeSession(_ context.Context, return &litrpc.RevokeSessionResponse{}, nil } +func (s *sessionRpcServer) PrivacyMapConversion(_ context.Context, + req *litrpc.PrivacyMapConversionRequest) ( + *litrpc.PrivacyMapConversionResponse, error) { + + sessionID, err := session.IDFromBytes(req.SessionId) + if err != nil { + return nil, err + } + + var res string + privMap := s.cfg.privMap(sessionID) + err = privMap.View(func(tx firewalldb.PrivacyMapTx) error { + var err error + if req.RealToPseudo { + res, err = tx.RealToPseudo(req.Input) + return err + } + + res, err = tx.PseudoToReal(req.Input) + return err + }) + if err != nil { + return nil, err + } + + return &litrpc.PrivacyMapConversionResponse{ + Output: res, + }, nil +} + +// ListActions lists all actions attempted on the Litd server. +func (s *sessionRpcServer) ListActions(_ context.Context, + req *litrpc.ListActionsRequest) (*litrpc.ListActionsResponse, error) { + + // If no maximum number of actions is given, use a default of 100. + if req.MaxNumActions == 0 { + req.MaxNumActions = 100 + } + + // Build a filter function based on the request values. + filterFn := func(a *firewalldb.Action, reversed bool) (bool, bool) { + timeStamp := uint64(a.AttemptedAt.Unix()) + if req.EndTimestamp != 0 { + // If actions are being considered in order and the + // timestamp of this action exceeds the given end + // timestamp, then there is no need to continue + // traversing. + if !reversed && timeStamp > req.EndTimestamp { + return false, false + } + + // If the actions are in reverse order and the timestamp + // comes after the end timestamp, then the actions is + // not included but the search can continue. + if reversed && timeStamp > req.EndTimestamp { + return false, true + } + } + + if req.StartTimestamp != 0 { + // If actions are being considered in order and the + // timestamp of this action comes before the given start + // timestamp, then the action is not included but the + // search can continue. + if !reversed && timeStamp < req.StartTimestamp { + return false, true + } + + // If the actions are in reverse order and the timestamp + // comes before the start timestamp, then there is no + // need to continue traversing. + if reversed && timeStamp < req.StartTimestamp { + return false, false + } + } + + if req.FeatureName != "" && a.FeatureName != req.FeatureName { + return false, true + } + + if req.ActorName != "" && a.ActorName != req.ActorName { + return false, true + } + + if req.MethodName != "" && a.RPCMethod != req.MethodName { + return false, true + } + + if req.State != 0 { + s, err := marshalActionState(a.State) + if err != nil { + return false, true + } + + if s != req.State { + return false, true + } + } + + return true, true + } + + query := &firewalldb.ListActionsQuery{ + IndexOffset: req.IndexOffset, + MaxNum: req.MaxNumActions, + Reversed: req.Reversed, + CountAll: req.CountTotal, + } + + var ( + db = s.cfg.actionsDB + actions []*firewalldb.Action + lastIndex uint64 + totalCount uint64 + err error + ) + if req.SessionId != nil { + sessionID, err := session.IDFromBytes(req.SessionId) + if err != nil { + return nil, err + } + + actions, lastIndex, totalCount, err = db.ListSessionActions( + sessionID, filterFn, query, + ) + if err != nil { + return nil, err + } + } else { + actions, lastIndex, totalCount, err = db.ListActions( + filterFn, query, + ) + if err != nil { + return nil, err + } + } + resp := make([]*litrpc.Action, len(actions)) + for i, a := range actions { + state, err := marshalActionState(a.State) + if err != nil { + return nil, err + } + + resp[i] = &litrpc.Action{ + SessionId: a.SessionID[:], + ActorName: a.ActorName, + FeatureName: a.FeatureName, + Trigger: a.Trigger, + Intent: a.Intent, + StructuredJsonData: a.StructuredJsonData, + RpcMethod: a.RPCMethod, + RpcParamsJson: string(a.RPCParamsJson), + Timestamp: uint64(a.AttemptedAt.Unix()), + State: state, + ErrorReason: a.ErrorReason, + } + } + + return &litrpc.ListActionsResponse{ + Actions: resp, + LastIndexOffset: lastIndex, + TotalCount: totalCount, + }, nil +} + +// ListAutopilotFeatures fetches all the features supported by the autopilot +// server along with the rules that we need to support in order to subscribe +// to those features. +func (s *sessionRpcServer) ListAutopilotFeatures(ctx context.Context, + _ *litrpc.ListAutopilotFeaturesRequest) ( + *litrpc.ListAutopilotFeaturesResponse, error) { + + fs, err := s.cfg.autopilot.ListFeatures(ctx) + if err != nil { + return nil, err + } + + features := make(map[string]*litrpc.Feature, len(fs)) + for i, f := range fs { + rules, upgrade, err := convertRules(s.cfg.ruleMgrs, f.Rules) + if err != nil { + return nil, err + } + + features[i] = &litrpc.Feature{ + Name: f.Name, + Description: f.Description, + Rules: rules, + PermissionsList: marshalPerms(f.Permissions), + RequiresUpgrade: upgrade, + } + } + + return &litrpc.ListAutopilotFeaturesResponse{ + Features: features, + }, nil +} + +// AddAutopilotSession creates a new LNC session and attempts to register it +// with the Autopilot server. +func (s *sessionRpcServer) AddAutopilotSession(ctx context.Context, + req *litrpc.AddAutopilotSessionRequest) ( + *litrpc.AddAutopilotSessionResponse, error) { + + if len(req.Features) == 0 { + return nil, fmt.Errorf("must include at least one feature") + } + + expiry := time.Unix(int64(req.ExpiryTimestampSeconds), 0) + if time.Now().After(expiry) { + return nil, fmt.Errorf("expiry must be in the future") + } + + privacy := !req.NoPrivacyMapper + privacyMapPairs := make(map[string]string) + + // First need to fetch all the perms that need to be baked into this + // mac based on the features. + allFeatures, err := s.cfg.autopilot.ListFeatures(ctx) + if err != nil { + return nil, err + } + + // Create lookup map of all the features that autopilot server offers. + autopilotFeatureMap := make(map[string]*autopilotserver.Feature) + for _, f := range allFeatures { + autopilotFeatureMap[f.Name] = f + } + + // allRules represents all the rules that our firewall knows about. + allRules := s.cfg.ruleMgrs.GetAllRules() + + // Check that each requested feature is a valid autopilot feature and + // that the necessary rules for the feature have been specified. + featureRules := make(map[string]map[string]string, len(req.Features)) + for f, rs := range req.Features { + // Check that the features is known by the autopilot server. + autopilotFeature, ok := autopilotFeatureMap[f] + if !ok { + return nil, fmt.Errorf("%s is not a features "+ + "provided by the Autopilot server", f) + } + + // reqRules is the rules specified in the request. + var reqRules []rules.Values + if rs.Rules != nil { + reqRules = make([]rules.Values, 0, len(rs.Rules.Rules)) + for ruleName, rule := range rs.Rules.Rules { + v, err := s.cfg.ruleMgrs.UnmarshalRuleValues( + ruleName, rule, + ) + if err != nil { + return nil, err + } + + if privacy { + var privMapPairs map[string]string + v, privMapPairs, err = v.RealToPseudo() + if err != nil { + return nil, err + } + + for k, v := range privMapPairs { + privacyMapPairs[k] = v + } + } + + reqRules = append(reqRules, v) + } + } + + // Create a lookup map for the rules specified in this feature. + // Also check that each of the rules in the request is one known + // to us. + frs := make(map[string]rules.Values) + for _, r := range reqRules { + frs[r.RuleName()] = r + _, ok := allRules[r.RuleName()] + if !ok { + return nil, fmt.Errorf("%s is not a known rule", + r.RuleName()) + } + } + + // For each of the specified rules, we check that the values + // set for those rules are sane given the bounds provided by + // the autopilot server for the given feature. + for _, r := range reqRules { + ruleName := r.RuleName() + + autopilotSpecs, ok := autopilotFeature.Rules[ruleName] + if !ok { + return nil, fmt.Errorf("autopilot did not "+ + "specify %s as a rule for feature %s", + ruleName, autopilotFeature.Name) + } + + min, err := s.cfg.ruleMgrs.InitRuleValues( + ruleName, autopilotSpecs.MinVal, + ) + if err != nil { + return nil, err + } + + max, err := s.cfg.ruleMgrs.InitRuleValues( + ruleName, autopilotSpecs.MaxVal, + ) + if err != nil { + return nil, err + } + + if err = r.VerifySane(min, max); err != nil { + return nil, fmt.Errorf("rule value for %s "+ + "not valid for feature %s. Expected "+ + "rule value between %s and %s. Got "+ + "%s. %v", ruleName, + autopilotFeature.Name, min, max, r, err) + } + } + + // If the request did not contain specific values for a rule, + // the default Autopilot rule value is used. + finalRules := make( + []rules.Values, 0, len(autopilotFeature.Rules), + ) + for name, values := range autopilotFeature.Rules { + if r, ok := frs[name]; ok { + finalRules = append(finalRules, r) + continue + } + + defaults, err := s.cfg.ruleMgrs.InitRuleValues( + name, values.Default, + ) + if err != nil { + return nil, err + } + + finalRules = append(finalRules, defaults) + } + + featureRules[f], err = marshalRulesToStringMap(finalRules) + if err != nil { + return nil, err + } + } + + interceptRules := &firewall.InterceptRules{ + FeatureRules: featureRules, + } + + // Gather all the permissions we need to add to the macaroon given the + // feature list. + var dedupedPerms = make(map[string]bool) + for name, feature := range autopilotFeatureMap { + if _, ok := req.Features[name]; !ok { + continue + } + + for uri := range feature.Permissions { + dedupedPerms[uri] = true + } + } + + var perms []bakery.Op + for uri := range dedupedPerms { + perms = append(perms, bakery.Op{ + Entity: macaroons.PermissionEntityCustomURI, + Action: uri, + }) + } + + rulesCaveatStr, err := firewall.RulesToCaveat(interceptRules) + if err != nil { + return nil, err + } + + featureConfig := make(map[string][]byte, len(req.Features)) + for name, f := range req.Features { + featureConfig[name] = f.Config + } + + caveats := []macaroon.Caveat{{Id: []byte(rulesCaveatStr)}} + if privacy { + caveats = append(caveats, firewall.MetaPrivacyCaveat) + } + + sess, err := session.NewSession( + req.Label, session.TypeAutopilot, expiry, req.MailboxServerAddr, + req.DevServer, perms, caveats, featureConfig, privacy, + ) + if err != nil { + return nil, fmt.Errorf("error creating new session: %v", err) + } + + // Register all the privacy map pairs for this session ID. + privDB := s.cfg.privMap(sess.ID) + err = privDB.Update(func(tx firewalldb.PrivacyMapTx) error { + for r, p := range privacyMapPairs { + err := tx.NewPair(r, p) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + return nil, err + } + + // Attempt to register the session with the Autopilot server. + remoteKey, err := s.cfg.autopilot.RegisterSession( + ctx, sess.LocalPublicKey, sess.ServerAddr, sess.DevServer, + featureConfig, + ) + if err != nil { + return nil, fmt.Errorf("error registering session with "+ + "autopilot server: %v", err) + } + + // We only persist this session if we successfully retrieved the + // autopilot's static key. + sess.RemotePublicKey = remoteKey + if err := s.db.StoreSession(sess); err != nil { + return nil, fmt.Errorf("error storing session: %v", err) + } + + if err := s.resumeSession(sess); err != nil { + return nil, fmt.Errorf("error starting session: %v", err) + } + + rpcSession, err := s.marshalRPCSession(sess) + if err != nil { + return nil, fmt.Errorf("error marshaling session: %v", err) + } + + return &litrpc.AddAutopilotSessionResponse{ + Session: rpcSession, + }, nil +} + +// ListAutopilotSessions fetches and returns all the sessions from the DB that +// are of type TypeAutopilot. +func (s *sessionRpcServer) ListAutopilotSessions(_ context.Context, + _ *litrpc.ListAutopilotSessionsRequest) ( + *litrpc.ListAutopilotSessionsResponse, error) { + + sessions, err := s.db.ListSessions(func(s *session.Session) bool { + return s.Type == session.TypeAutopilot + }) + if err != nil { + return nil, fmt.Errorf("error fetching sessions: %v", err) + } + + response := &litrpc.ListAutopilotSessionsResponse{ + Sessions: make([]*litrpc.Session, len(sessions)), + } + for idx, sess := range sessions { + response.Sessions[idx], err = s.marshalRPCSession(sess) + if err != nil { + return nil, fmt.Errorf("error marshaling session: %v", + err) + } + } + + return response, nil +} + +// RevokeAutopilotSession revokes an autopilot session. +func (s *sessionRpcServer) RevokeAutopilotSession(ctx context.Context, + req *litrpc.RevokeAutopilotSessionRequest) ( + *litrpc.RevokeAutopilotSessionResponse, error) { + + pubKey, err := btcec.ParsePubKey(req.LocalPublicKey) + if err != nil { + return nil, fmt.Errorf("error parsing public key: %v", err) + } + + sess, err := s.db.GetSession(pubKey) + if err != nil { + return nil, err + } + + if sess.Type != session.TypeAutopilot { + return nil, session.ErrSessionNotFound + } + + _, err = s.RevokeSession( + ctx, &litrpc.RevokeSessionRequest{ + LocalPublicKey: req.LocalPublicKey, + }, + ) + if err != nil { + return nil, err + } + + return &litrpc.RevokeAutopilotSessionResponse{}, nil +} + +func marshalRulesToStringMap(rs []rules.Values) (map[string]string, error) { + res := make(map[string]string, len(rs)) + for _, r := range rs { + b, err := json.Marshal(r) + if err != nil { + return nil, err + } + + res[r.RuleName()] = string(b) + } + + return res, nil +} + +// marshalPerms attempts to convert a set of permissions into their RPC +// counterpart. If the list includes a rule that LiT does not know about, a nil +// entry is included in the returned map. A bool is returned that indicates if +// an upgrade is needed in order to enforce an unknown rule. +func convertRules(ruleMgr rules.ManagerSet, + ruleList map[string]*autopilotserver.RuleValues) ( + map[string]*litrpc.RuleValues, bool, error) { + + var ( + upgrade bool + res = make( + map[string]*litrpc.RuleValues, len(ruleList), + ) + knownRules = ruleMgr.GetAllRules() + ) + for name, rule := range ruleList { + known := true + if !knownRules[name] { + upgrade = true + known = false + } + + defaultVals, err := ruleMgr.InitRuleValues(name, rule.Default) + if err != nil { + return nil, false, err + } + + minVals, err := ruleMgr.InitRuleValues(name, rule.MinVal) + if err != nil { + return nil, false, err + } + + maxVals, err := ruleMgr.InitRuleValues(name, rule.MaxVal) + if err != nil { + return nil, false, err + } + + res[name] = &litrpc.RuleValues{ + Known: known, + Defaults: defaultVals.ToProto(), + MinValue: minVals.ToProto(), + MaxValue: maxVals.ToProto(), + } + } + + return res, upgrade, nil +} + +// marshalPerms converts a set of permissions into their RPC counterpart. +func marshalPerms(perms map[string][]bakery.Op) []*litrpc.Permissions { + var res []*litrpc.Permissions + + for method, ops := range perms { + rpcOps := make([]*litrpc.MacaroonPermission, len(ops)) + for i, op := range ops { + rpcOps[i] = &litrpc.MacaroonPermission{ + Entity: op.Entity, + Action: op.Action, + } + } + + res = append(res, &litrpc.Permissions{ + Method: method, + Operations: rpcOps, + }) + } + + return res +} + // marshalRPCSession converts a session into its RPC counterpart. -func marshalRPCSession(sess *session.Session) (*litrpc.Session, error) { +func (s *sessionRpcServer) marshalRPCSession(sess *session.Session) ( + *litrpc.Session, error) { + rpcState, err := marshalRPCState(sess.State) if err != nil { return nil, err @@ -524,7 +1195,42 @@ func marshalRPCSession(sess *session.Session) (*litrpc.Session, error) { revokedAt = uint64(sess.RevokedAt.Unix()) } + featureInfo := make(map[string]*litrpc.RulesMap) + if sess.MacaroonRecipe != nil { + for _, cav := range sess.MacaroonRecipe.Caveats { + info, err := firewall.ParseRuleCaveat(string(cav.Id)) + if errors.Is(err, firewall.ErrNoRulesCaveat) { + continue + } else if err != nil { + return nil, err + } + + for feature, rules := range info.FeatureRules { + ruleMap := make(map[string]*litrpc.RuleValue) + for name, rule := range rules { + val, err := s.cfg.ruleMgrs.InitRuleValues(name, []byte(rule)) + if err != nil { + return nil, err + } + + if sess.WithPrivacyMapper { + db := s.cfg.privMap(sess.ID) + val, err = val.PseudoToReal(db) + if err != nil { + return nil, err + } + } + + ruleMap[name] = val.ToProto() + } + + featureInfo[feature] = &litrpc.RulesMap{Rules: ruleMap} + } + } + } + return &litrpc.Session{ + Id: sess.ID[:], Label: sess.Label, SessionState: rpcState, SessionType: rpcType, @@ -538,6 +1244,7 @@ func marshalRPCSession(sess *session.Session) (*litrpc.Session, error) { CreatedAt: uint64(sess.CreatedAt.Unix()), RevokedAt: revokedAt, MacaroonRecipe: macRecipe, + AutopilotFeatureInfo: featureInfo, }, nil } @@ -607,6 +1314,9 @@ func marshalRPCType(typ session.Type) (litrpc.SessionType, error) { case session.TypeMacaroonAccount: return litrpc.SessionType_TYPE_MACAROON_ACCOUNT, nil + case session.TypeAutopilot: + return litrpc.SessionType_TYPE_AUTOPILOT, nil + default: return 0, fmt.Errorf("unknown type <%d>", typ) } @@ -630,7 +1340,28 @@ func unmarshalRPCType(typ litrpc.SessionType) (session.Type, error) { case litrpc.SessionType_TYPE_MACAROON_ACCOUNT: return session.TypeMacaroonAccount, nil + case litrpc.SessionType_TYPE_AUTOPILOT: + return session.TypeAutopilot, nil + default: return 0, fmt.Errorf("unknown type <%d>", typ) } } + +// marshalActionState converts an Action state into its RPC counterpart. +func marshalActionState(state firewalldb.ActionState) (litrpc.ActionState, + error) { + + switch state { + case firewalldb.ActionStateUnknown: + return litrpc.ActionState_STATE_UNKNOWN, nil + case firewalldb.ActionStateInit: + return litrpc.ActionState_STATE_PENDING, nil + case firewalldb.ActionStateDone: + return litrpc.ActionState_STATE_DONE, nil + case firewalldb.ActionStateError: + return litrpc.ActionState_STATE_ERROR, nil + default: + return 0, fmt.Errorf("unknown state <%d>", state) + } +} diff --git a/terminal.go b/terminal.go index 800ddb011..550f1612e 100644 --- a/terminal.go +++ b/terminal.go @@ -22,10 +22,14 @@ import ( "github.com/lightninglabs/faraday/frdrpc" "github.com/lightninglabs/faraday/frdrpcserver" "github.com/lightninglabs/lightning-terminal/accounts" + "github.com/lightninglabs/lightning-terminal/autopilotserver" + "github.com/lightninglabs/lightning-terminal/firewall" + "github.com/lightninglabs/lightning-terminal/firewalldb" "github.com/lightninglabs/lightning-terminal/litrpc" "github.com/lightninglabs/lightning-terminal/perms" "github.com/lightninglabs/lightning-terminal/queue" mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" + "github.com/lightninglabs/lightning-terminal/rules" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop" @@ -61,6 +65,9 @@ import ( ) const ( + MainnetServer = "autopilot.lightning.finance:12010" + TestnetServer = "test.autopilot.lightning.finance:12010" + defaultServerTimeout = 10 * time.Second defaultConnectTimeout = 15 * time.Second defaultStartupTimeout = 5 * time.Second @@ -153,6 +160,10 @@ type LightningTerminal struct { faradayServer *frdrpcserver.RPCServer faradayStarted bool + autopilotClient autopilotserver.Autopilot + + ruleMgrs rules.ManagerSet + loopServer *loopd.Daemon loopStarted bool @@ -176,6 +187,8 @@ type LightningTerminal struct { accountRpcServer *accounts.RPCServer + firewallDB *firewalldb.DB + restHandler http.Handler restCancel func() } @@ -245,6 +258,44 @@ func (g *LightningTerminal) Run() error { g.accountRpcServer = accounts.NewRPCServer( g.accountService, superMacBaker, ) + + g.ruleMgrs = rules.NewRuleManagerSet() + + networkDir := filepath.Join(g.cfg.LitDir, g.cfg.Network) + g.firewallDB, err = firewalldb.NewDB(networkDir, firewalldb.DBFilename) + if err != nil { + return fmt.Errorf("error creating session DB: %v", err) + } + + if !g.cfg.Autopilot.Disable { + if g.cfg.Autopilot.Address == "" && + len(g.cfg.Autopilot.DialOpts) == 0 { + + switch g.cfg.Network { + case "mainnet": + g.cfg.Autopilot.Address = MainnetServer + case "testnet": + g.cfg.Autopilot.Address = TestnetServer + default: + return errors.New("no autopilot server " + + "address specified") + } + } + + g.cfg.Autopilot.LitVersion = autopilotserver.Version{ + Major: uint32(appMajor), + Minor: uint32(appMinor), + Patch: uint32(appPatch), + } + + g.autopilotClient, err = autopilotserver.NewClient( + g.cfg.Autopilot, + ) + if err != nil { + return err + } + } + g.sessionRpcServer, err = newSessionRPCServer(&sessionRpcServerConfig{ basicAuth: g.rpcProxy.basicAuth, dbDir: filepath.Join(g.cfg.LitDir, g.cfg.Network), @@ -269,6 +320,10 @@ func (g *LightningTerminal) Run() error { superMacBaker: superMacBaker, firstConnectionDeadline: g.cfg.FirstLNCConnDeadline, permMgr: g.permsMgr, + actionsDB: g.firewallDB, + autopilot: g.autopilotClient, + ruleMgrs: g.ruleMgrs, + privMap: g.firewallDB.PrivacyDB, }) if err != nil { return fmt.Errorf("could not create new session rpc "+ @@ -632,37 +687,97 @@ func (g *LightningTerminal) startSubservers() error { } g.macaroonServiceStarted = true + if !g.cfg.Autopilot.Disable { + withLndVersion := func(cfg *autopilotserver.Config) { + cfg.LndVersion = autopilotserver.Version{ + Major: g.lndClient.Version.AppMajor, + Minor: g.lndClient.Version.AppMinor, + Patch: g.lndClient.Version.AppPatch, + } + } + + if err = g.autopilotClient.Start(withLndVersion); err != nil { + return fmt.Errorf("could not start the autopilot "+ + "client: %v", err) + } + } + log.Infof("Starting LiT session server") if err = g.sessionRpcServer.start(); err != nil { return err } g.sessionRpcServerStarted = true - if !g.cfg.RPCMiddleware.Disabled { - log.Infof("Starting LiT account service") - err := g.accountService.Start( - g.lndClient.Client, g.lndClient.Router, - g.lndClient.ChainParams, - ) + // The rest of the function only applies if the rpc middleware + // interceptor has been enabled. + if g.cfg.RPCMiddleware.Disabled { + log.Infof("Internal sub server startup complete") + + return nil + } + + log.Infof("Starting LiT account service") + err = g.accountService.Start( + g.lndClient.Client, g.lndClient.Router, + g.lndClient.ChainParams, + ) + if err != nil { + return fmt.Errorf("error starting account service: %v", + err) + } + g.accountServiceStarted = true + + requestLogger, err := firewall.NewRequestLogger( + g.cfg.Firewall.RequestLogger, g.firewallDB, + ) + if err != nil { + return fmt.Errorf("error creating new request logger") + } + + privacyMapper := firewall.NewPrivacyMapper( + g.firewallDB.PrivacyDB, firewall.CryptoRandIntn, + ) + + mw := []mid.RequestInterceptor{ + privacyMapper, + g.accountService, + requestLogger, + } + + if !g.cfg.Autopilot.Disable { + info, err := g.lndClient.Client.GetInfo(ctxc) if err != nil { - return fmt.Errorf("error starting account service: %v", - err) + return fmt.Errorf("GetInfo call failed: %v", err) } - g.accountServiceStarted = true - - // Start the middleware manager. - log.Infof("Starting LiT middleware manager") - g.middleware = mid.NewManager( - g.cfg.RPCMiddleware.InterceptTimeout, - g.lndClient.Client, g.errQueue.ChanIn(), - g.accountService, + + ruleEnforcer := firewall.NewRuleEnforcer( + g.firewallDB, g.firewallDB, + g.autopilotClient.ListFeaturePerms, + g.permsMgr, info.IdentityPubkey, + g.lndClient.Router, + g.lndClient.Client, g.ruleMgrs, + func(reqID uint64, reason string) error { + return requestLogger.MarkAction( + reqID, firewalldb.ActionStateError, + reason, + ) + }, g.firewallDB.PrivacyDB, ) - if err = g.middleware.Start(); err != nil { - return err - } - g.middlewareStarted = true + mw = append(mw, ruleEnforcer) + } + + // Start the middleware manager. + log.Infof("Starting LiT middleware manager") + g.middleware = mid.NewManager( + g.cfg.RPCMiddleware.InterceptTimeout, + g.lndClient.Client, g.errQueue.ChanIn(), mw..., + ) + + if err = g.middleware.Start(); err != nil { + return err } + g.middlewareStarted = true log.Infof("Internal sub server startup complete") @@ -715,6 +830,12 @@ func (g *LightningTerminal) registerSubDaemonGrpcServers(server *grpc.Server, litrpc.RegisterSessionsServer(server, g.sessionRpcServer) litrpc.RegisterAccountsServer(server, g.accountRpcServer) } + + litrpc.RegisterFirewallServer(server, g.sessionRpcServer) + + if !g.cfg.Autopilot.Disable { + litrpc.RegisterAutopilotServer(server, g.sessionRpcServer) + } } // RegisterRestSubserver is a callback on the lnd.SubserverConfig struct that is @@ -941,6 +1062,10 @@ func (g *LightningTerminal) shutdown() error { } } + if g.autopilotClient != nil { + g.autopilotClient.Stop() + } + if g.sessionRpcServerStarted { if err := g.sessionRpcServer.stop(); err != nil { log.Errorf("Error closing session DB: %v", err) @@ -966,6 +1091,20 @@ func (g *LightningTerminal) shutdown() error { g.middleware.Stop() } + if g.firewallDB != nil { + if err := g.firewallDB.Close(); err != nil { + log.Errorf("Error closing rules DB: %v", err) + returnErr = err + } + } + + if g.ruleMgrs != nil { + if err := g.ruleMgrs.Stop(); err != nil { + log.Errorf("Error stopping rule manager set: %v", err) + returnErr = err + } + } + if g.lndClient != nil { g.lndClient.Close() }