From 87dee4b15a9595ab6e5c7ba3b1050507ecf319c3 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 9 Jan 2018 12:58:13 +0100 Subject: [PATCH 01/39] lntest: add AddToLog method for node This commit adds a new method, AddToLog, that can be used to add strings to a test node's log file. --- lntest/node.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lntest/node.go b/lntest/node.go index 8f018891..1e2fd7b7 100644 --- a/lntest/node.go +++ b/lntest/node.go @@ -163,6 +163,7 @@ type HarnessNode struct { cmd *exec.Cmd pidFile string + logFile *os.File // processExit is a channel that's closed once it's detected that the // process this instance of HarnessNode is bound to has exited. @@ -231,10 +232,10 @@ func (hn *HarnessNode) start(lndError chan<- error) error { // If the logoutput flag is passed, redirect output from the nodes to // log files. if *logOutput { - logFile := fmt.Sprintf("output%d.log", hn.NodeID) + fileName := fmt.Sprintf("output%d.log", hn.NodeID) // Create file if not exists, otherwise append. - file, err := os.OpenFile(logFile, + file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) if err != nil { return err @@ -246,6 +247,10 @@ func (hn *HarnessNode) start(lndError chan<- error) error { // Pass the node's stdout only to the file. hn.cmd.Stdout = file + + // Let the node keep a reference to this file, such + // that we can add to it if necessary. + hn.logFile = file } if err := hn.cmd.Start(); err != nil { @@ -304,6 +309,19 @@ func (hn *HarnessNode) start(lndError chan<- error) error { return nil } +// AddToLog adds a line of choice to the node's logfile. This is useful +// to interleave test output with output from the node. +func (hn *HarnessNode) AddToLog(line string) error { + // If this node was not set up with a log file, just return early. + if hn.logFile == nil { + return nil + } + if _, err := hn.logFile.WriteString(line); err != nil { + return err + } + return nil +} + // writePidFile writes the process ID of the running lnd process to a .pid file. func (hn *HarnessNode) writePidFile() error { filePath := filepath.Join(hn.cfg.BaseDir, fmt.Sprintf("%v.pid", hn.NodeID)) From 4a1a9d6b14d27fc2418c159eeb9fcb89b9f751fd Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 9 Jan 2018 12:59:32 +0100 Subject: [PATCH 02/39] lnd_test: add name of testcase to node's logfile This commit adds a line of text including a test case's name to Alice's and Bob's logfiles during integration tests, making it easier to seek for the place in the log where the specific tests start. --- lnd_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lnd_test.go b/lnd_test.go index 63212522..0292e278 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -4847,6 +4847,15 @@ func TestLightningNetworkDaemon(t *testing.T) { t.Logf("Running %v integration tests", len(testsCases)) for _, testCase := range testsCases { + logLine := fmt.Sprintf("STARTING ============ %v ============\n", + testCase.name) + if err := lndHarness.Alice.AddToLog(logLine); err != nil { + t.Fatalf("unable to add to log: %v", err) + } + if err := lndHarness.Bob.AddToLog(logLine); err != nil { + t.Fatalf("unable to add to log: %v", err) + } + success := t.Run(testCase.name, func(t1 *testing.T) { ht := newHarnessTest(t1) ht.RunTestCase(testCase, lndHarness) From c17b695128177a6449cf938ec5b278d35aab66fb Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Tue, 9 Jan 2018 18:14:25 -0800 Subject: [PATCH 03/39] lnwallet/btcwallet/blockchain: properly handle nil spend report This commit adds an additional check in GetUtxo that tests for the nil-ness of the spend report returned by the neutrino backend. Previously, a nil error and spend report could be returned if the rescan did not find the output at or above the start height. This was observed to have cause a nil pointer dereference when the returning line attempted to access the output. This case is now handled by returning a distinct error signaling that the output was not found. --- lnwallet/btcwallet/blockchain.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lnwallet/btcwallet/blockchain.go b/lnwallet/btcwallet/blockchain.go index fed6c830..d930ac55 100644 --- a/lnwallet/btcwallet/blockchain.go +++ b/lnwallet/btcwallet/blockchain.go @@ -18,6 +18,10 @@ var ( // ErrOutputSpent is returned by the GetUtxo method if the target output // for lookup has already been spent. ErrOutputSpent = errors.New("target output has been spent") + + // ErrOutputNotFound signals that the desired output could not be + // located. + ErrOutputNotFound = errors.New("target output was not found") ) // GetBestBlock returns the current height and hash of the best known block @@ -61,10 +65,19 @@ func (b *BtcWallet) GetUtxo(op *wire.OutPoint, heightHint uint32) (*wire.TxOut, return nil, err } - if spendReport != nil && spendReport.SpendingTx != nil { + // If the spend report is nil, then the output was not found in + // the rescan. + if spendReport == nil { + return nil, ErrOutputNotFound + } + + // If the spending transaction is populated in the spend report, + // this signals that the output has already been spent. + if spendReport.SpendingTx != nil { return nil, ErrOutputSpent } + // Otherwise, the output is assumed to be in the UTXO. return spendReport.Output, nil case *chain.RPCClient: From 9bdb483757f3cf914a0e8484a6d1f3c83bba9022 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 15 Dec 2017 21:50:21 +0100 Subject: [PATCH 04/39] htlcswitch/link: make MinHTLC non-changable This commit specifies that the MinHTLC value for a link is static over the lifetime of a channel, and don't process the field during a policyUpdate. --- htlcswitch/link.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 10e8d91b..f63a1a06 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -39,7 +39,9 @@ const ( // the error possibly carrying along a ChannelUpdate message that includes the // latest policy. type ForwardingPolicy struct { - // MinHTLC is the smallest HTLC that is to be forwarded. + // MinHTLC is the smallest HTLC that is to be forwarded. This is + // set when a channel is first opened, and will be static for the + // lifetime of the channel. MinHTLC lnwire.MilliSatoshi // BaseFee is the base fee, expressed in milli-satoshi that must be @@ -604,9 +606,6 @@ out: // with a "null" field in the new policy, we'll // only update to the set sub policy if the new // value isn't uninitialized. - if req.policy.MinHTLC != 0 { - l.cfg.FwrdingPolicy.MinHTLC = req.policy.MinHTLC - } if req.policy.BaseFee != 0 { l.cfg.FwrdingPolicy.BaseFee = req.policy.BaseFee } From 0b8e7ff83654b37a51b6f1611e4722f438bf9666 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 15 Dec 2017 21:54:07 +0100 Subject: [PATCH 05/39] routing: embed FeeSchema in ChannelPolicy This commit embeds the FeeSchema within the new struct ChannelPolicy, which also contains the TimeLockDelta for a channel. --- routing/router.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/routing/router.go b/routing/router.go index 5fae3c7f..8ffebc84 100644 --- a/routing/router.go +++ b/routing/router.go @@ -94,6 +94,18 @@ type FeeSchema struct { FeeRate uint32 } +// ChannelPolicy holds the parameters that determine the policy we enforce +// when fowarding payments on a channel. These parameters are communicated +// to the rest of the network in ChannelUpdate messages. +type ChannelPolicy struct { + // FeeSchema holds the fee configuration for a channel. + FeeSchema + + // TimeLockDelta is the required HTLC timelock delta to be used + // when forwarding payments. + TimeLockDelta uint32 +} + // Config defines the configuration for the ChannelRouter. ALL elements within // the configuration MUST be non-nil for the ChannelRouter to carry out its // duties. From 8370fa2cde159329ee4aa97c3b89ca8efd49a8e7 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 15 Dec 2017 21:56:11 +0100 Subject: [PATCH 06/39] discovery/gossiper: apply TimeLockDelta to edge when processing policy update Also rename various instances of "FeeUpdate" to "PolicyUpdate" --- discovery/gossiper.go | 73 +++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/discovery/gossiper.go b/discovery/gossiper.go index 87ad835b..bf876257 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -42,13 +42,13 @@ type networkMsg struct { err chan error } -// feeUpdateRequest is a request that is sent to the server when a caller -// wishes to update the fees for a particular set of channels. New UpdateFee -// messages will be crafted to be sent out during the next broadcast epoch and -// the fee updates committed to the lower layer. -type feeUpdateRequest struct { +// chanPolicyUpdateRequest is a request that is sent to the server when a caller +// wishes to update the channel policy (fees e.g.) for a particular set of +// channels. New ChannelUpdate messages will be crafted to be sent out during +// the next broadcast epoch and the fee updates committed to the lower layer. +type chanPolicyUpdateRequest struct { targetChans []wire.OutPoint - newSchema routing.FeeSchema + newSchema routing.ChannelPolicy errResp chan error } @@ -175,9 +175,9 @@ type AuthenticatedGossiper struct { // networkHandler. networkMsgs chan *networkMsg - // feeUpdates is a channel that requests to update the fee schedule of - // a set of channels is sent over. - feeUpdates chan *feeUpdateRequest + // chanPolicyUpdates is a channel that requests to update the forwarding + // policy of a set of channels is sent over. + chanPolicyUpdates chan *chanPolicyUpdateRequest // bestHeight is the height of the block at the tip of the main chain // as we know it. @@ -202,7 +202,7 @@ func New(cfg Config, selfKey *btcec.PublicKey) (*AuthenticatedGossiper, error) { cfg: &cfg, networkMsgs: make(chan *networkMsg), quit: make(chan struct{}), - feeUpdates: make(chan *feeUpdateRequest), + chanPolicyUpdates: make(chan *chanPolicyUpdateRequest), prematureAnnouncements: make(map[uint32][]*networkMsg), prematureChannelUpdates: make(map[uint64][]*networkMsg), waitingProofs: storage, @@ -296,24 +296,24 @@ func (d *AuthenticatedGossiper) SynchronizeNode(pub *btcec.PublicKey) error { return d.cfg.SendToPeer(pub, announceMessages...) } -// PropagateFeeUpdate signals the AuthenticatedGossiper to update the fee -// schema for the specified channels. If no channels are specified, then the -// fee update will be applied to all outgoing channels from the source node. -// Fee updates are done in two stages: first, the AuthenticatedGossiper ensures -// the updated has been committed by dependant sub-systems, then it signs and -// broadcasts new updates to the network. -func (d *AuthenticatedGossiper) PropagateFeeUpdate(newSchema routing.FeeSchema, - chanPoints ...wire.OutPoint) error { +// PropagateChanPolicyUpdate signals the AuthenticatedGossiper to update the +// channel forwarding policies for the specified channels. If no channels are +// specified, then the update will be applied to all outgoing channels from the +// source node. Policy updates are done in two stages: first, the +// AuthenticatedGossiper ensures the update has been committed by dependant +// sub-systems, then it signs and broadcasts new updates to the network. +func (d *AuthenticatedGossiper) PropagateChanPolicyUpdate( + newSchema routing.ChannelPolicy, chanPoints ...wire.OutPoint) error { errChan := make(chan error, 1) - feeUpdate := &feeUpdateRequest{ + policyUpdate := &chanPolicyUpdateRequest{ targetChans: chanPoints, newSchema: newSchema, errResp: errChan, } select { - case d.feeUpdates <- feeUpdate: + case d.chanPolicyUpdates <- policyUpdate: return <-errChan case <-d.quit: return fmt.Errorf("AuthenticatedGossiper shutting down") @@ -823,17 +823,18 @@ func (d *AuthenticatedGossiper) networkHandler() { for { select { - // A new fee update has arrived. We'll commit it to the + // A new policy update has arrived. We'll commit it to the // sub-systems below us, then craft, sign, and broadcast a new // ChannelUpdate for the set of affected clients. - case feeUpdate := <-d.feeUpdates: + case policyUpdate := <-d.chanPolicyUpdates: // First, we'll now create new fully signed updates for // the affected channels and also update the underlying // graph with the new state. - newChanUpdates, err := d.processFeeChanUpdate(feeUpdate) + newChanUpdates, err := d.processChanPolicyUpdate(policyUpdate) if err != nil { - log.Errorf("Unable to craft fee updates: %v", err) - feeUpdate.errResp <- err + log.Errorf("Unable to craft policy updates: %v", + err) + policyUpdate.errResp <- err continue } @@ -842,7 +843,7 @@ func (d *AuthenticatedGossiper) networkHandler() { // start of the next epoch. announcements.AddMsgs(newChanUpdates...) - feeUpdate.errResp <- nil + policyUpdate.errResp <- nil case announcement := <-d.networkMsgs: // Channel annoucnement signatures are the only message @@ -1072,18 +1073,19 @@ func (d *AuthenticatedGossiper) retransmitStaleChannels() error { return nil } -// processFeeChanUpdate generates a new set of channel updates with the new fee -// schema applied for each specified channel identified by its channel point. -// In the case that no channel points are specified, then the fee update will +// processChanPolicyUpdate generates a new set of channel updates with the new +// channel policy applied for each specified channel identified by its channel +// point. In the case that no channel points are specified, then the update will // be applied to all channels. Finally, the backing ChannelGraphSource is -// updated with the latest information reflecting the applied fee updates. +// updated with the latest information reflecting the applied updates. // // TODO(roasbeef): generalize into generic for any channel update -func (d *AuthenticatedGossiper) processFeeChanUpdate(feeUpdate *feeUpdateRequest) ([]networkMsg, error) { +func (d *AuthenticatedGossiper) processChanPolicyUpdate( + policyUpdate *chanPolicyUpdateRequest) ([]networkMsg, error) { // First, we'll construct a set of all the channels that need to be // updated. chansToUpdate := make(map[wire.OutPoint]struct{}) - for _, chanPoint := range feeUpdate.targetChans { + for _, chanPoint := range policyUpdate.targetChans { chansToUpdate[chanPoint] = struct{}{} } @@ -1104,11 +1106,14 @@ func (d *AuthenticatedGossiper) processFeeChanUpdate(feeUpdate *feeUpdateRequest } // Apply the new fee schema to the edge. - edge.FeeBaseMSat = feeUpdate.newSchema.BaseFee + edge.FeeBaseMSat = policyUpdate.newSchema.BaseFee edge.FeeProportionalMillionths = lnwire.MilliSatoshi( - feeUpdate.newSchema.FeeRate, + policyUpdate.newSchema.FeeRate, ) + // Apply the new TimeLockDelta. + edge.TimeLockDelta = uint16(policyUpdate.newSchema.TimeLockDelta) + // Re-sign and update the backing ChannelGraphSource, and // retrieve our ChannelUpdate to broadcast. _, chanUpdate, err := d.updateChannel(info, edge) From 551326586c200f8d158dc485d9fb6b66e05a42cc Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 15 Dec 2017 22:11:23 +0100 Subject: [PATCH 07/39] lnrpc: rename UpdateFee -> UpdateChannelPolicy This commit renames the UpdateFee RPC call together with associated types to UpdateChannelPolicy. In addition to fees, now also timelock delta can be specified using this call. --- lnrpc/rpc.pb.go | 740 +++++++++++++++++++++-------------------- lnrpc/rpc.pb.gw.go | 16 +- lnrpc/rpc.proto | 19 +- lnrpc/rpc.swagger.json | 67 ++-- 4 files changed, 430 insertions(+), 412 deletions(-) diff --git a/lnrpc/rpc.pb.go b/lnrpc/rpc.pb.go index caf2a797..442ad099 100644 --- a/lnrpc/rpc.pb.go +++ b/lnrpc/rpc.pb.go @@ -98,8 +98,8 @@ It has these top-level messages: FeeReportRequest ChannelFeeReport FeeReportResponse - FeeUpdateRequest - FeeUpdateResponse + PolicyUpdateRequest + PolicyUpdateResponse */ package lnrpc @@ -3477,111 +3477,120 @@ func (m *FeeReportResponse) GetChannelFees() []*ChannelFeeReport { return nil } -type FeeUpdateRequest struct { +type PolicyUpdateRequest struct { // Types that are valid to be assigned to Scope: - // *FeeUpdateRequest_Global - // *FeeUpdateRequest_ChanPoint - Scope isFeeUpdateRequest_Scope `protobuf_oneof:"scope"` + // *PolicyUpdateRequest_Global + // *PolicyUpdateRequest_ChanPoint + Scope isPolicyUpdateRequest_Scope `protobuf_oneof:"scope"` // / The base fee charged regardless of the number of milli-satoshis sent. BaseFeeMsat int64 `protobuf:"varint,3,opt,name=base_fee_msat" json:"base_fee_msat,omitempty"` // / The effective fee rate in milli-satoshis. The precision of this value goes up to 6 decimal places, so 1e-6. FeeRate float64 `protobuf:"fixed64,4,opt,name=fee_rate" json:"fee_rate,omitempty"` + // / The required timelock delta for HTLCs forwarded over the channel. + TimeLockDelta uint32 `protobuf:"varint,5,opt,name=time_lock_delta" json:"time_lock_delta,omitempty"` } -func (m *FeeUpdateRequest) Reset() { *m = FeeUpdateRequest{} } -func (m *FeeUpdateRequest) String() string { return proto.CompactTextString(m) } -func (*FeeUpdateRequest) ProtoMessage() {} -func (*FeeUpdateRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{90} } +func (m *PolicyUpdateRequest) Reset() { *m = PolicyUpdateRequest{} } +func (m *PolicyUpdateRequest) String() string { return proto.CompactTextString(m) } +func (*PolicyUpdateRequest) ProtoMessage() {} +func (*PolicyUpdateRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{90} } -type isFeeUpdateRequest_Scope interface { - isFeeUpdateRequest_Scope() +type isPolicyUpdateRequest_Scope interface { + isPolicyUpdateRequest_Scope() } -type FeeUpdateRequest_Global struct { +type PolicyUpdateRequest_Global struct { Global bool `protobuf:"varint,1,opt,name=global,oneof"` } -type FeeUpdateRequest_ChanPoint struct { +type PolicyUpdateRequest_ChanPoint struct { ChanPoint *ChannelPoint `protobuf:"bytes,2,opt,name=chan_point,oneof"` } -func (*FeeUpdateRequest_Global) isFeeUpdateRequest_Scope() {} -func (*FeeUpdateRequest_ChanPoint) isFeeUpdateRequest_Scope() {} +func (*PolicyUpdateRequest_Global) isPolicyUpdateRequest_Scope() {} +func (*PolicyUpdateRequest_ChanPoint) isPolicyUpdateRequest_Scope() {} -func (m *FeeUpdateRequest) GetScope() isFeeUpdateRequest_Scope { +func (m *PolicyUpdateRequest) GetScope() isPolicyUpdateRequest_Scope { if m != nil { return m.Scope } return nil } -func (m *FeeUpdateRequest) GetGlobal() bool { - if x, ok := m.GetScope().(*FeeUpdateRequest_Global); ok { +func (m *PolicyUpdateRequest) GetGlobal() bool { + if x, ok := m.GetScope().(*PolicyUpdateRequest_Global); ok { return x.Global } return false } -func (m *FeeUpdateRequest) GetChanPoint() *ChannelPoint { - if x, ok := m.GetScope().(*FeeUpdateRequest_ChanPoint); ok { +func (m *PolicyUpdateRequest) GetChanPoint() *ChannelPoint { + if x, ok := m.GetScope().(*PolicyUpdateRequest_ChanPoint); ok { return x.ChanPoint } return nil } -func (m *FeeUpdateRequest) GetBaseFeeMsat() int64 { +func (m *PolicyUpdateRequest) GetBaseFeeMsat() int64 { if m != nil { return m.BaseFeeMsat } return 0 } -func (m *FeeUpdateRequest) GetFeeRate() float64 { +func (m *PolicyUpdateRequest) GetFeeRate() float64 { if m != nil { return m.FeeRate } return 0 } +func (m *PolicyUpdateRequest) GetTimeLockDelta() uint32 { + if m != nil { + return m.TimeLockDelta + } + return 0 +} + // XXX_OneofFuncs is for the internal use of the proto package. -func (*FeeUpdateRequest) XXX_OneofFuncs() (func(msg proto.Message, b *proto.Buffer) error, func(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error), func(msg proto.Message) (n int), []interface{}) { - return _FeeUpdateRequest_OneofMarshaler, _FeeUpdateRequest_OneofUnmarshaler, _FeeUpdateRequest_OneofSizer, []interface{}{ - (*FeeUpdateRequest_Global)(nil), - (*FeeUpdateRequest_ChanPoint)(nil), +func (*PolicyUpdateRequest) XXX_OneofFuncs() (func(msg proto.Message, b *proto.Buffer) error, func(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error), func(msg proto.Message) (n int), []interface{}) { + return _PolicyUpdateRequest_OneofMarshaler, _PolicyUpdateRequest_OneofUnmarshaler, _PolicyUpdateRequest_OneofSizer, []interface{}{ + (*PolicyUpdateRequest_Global)(nil), + (*PolicyUpdateRequest_ChanPoint)(nil), } } -func _FeeUpdateRequest_OneofMarshaler(msg proto.Message, b *proto.Buffer) error { - m := msg.(*FeeUpdateRequest) +func _PolicyUpdateRequest_OneofMarshaler(msg proto.Message, b *proto.Buffer) error { + m := msg.(*PolicyUpdateRequest) // scope switch x := m.Scope.(type) { - case *FeeUpdateRequest_Global: + case *PolicyUpdateRequest_Global: t := uint64(0) if x.Global { t = 1 } b.EncodeVarint(1<<3 | proto.WireVarint) b.EncodeVarint(t) - case *FeeUpdateRequest_ChanPoint: + case *PolicyUpdateRequest_ChanPoint: b.EncodeVarint(2<<3 | proto.WireBytes) if err := b.EncodeMessage(x.ChanPoint); err != nil { return err } case nil: default: - return fmt.Errorf("FeeUpdateRequest.Scope has unexpected type %T", x) + return fmt.Errorf("PolicyUpdateRequest.Scope has unexpected type %T", x) } return nil } -func _FeeUpdateRequest_OneofUnmarshaler(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error) { - m := msg.(*FeeUpdateRequest) +func _PolicyUpdateRequest_OneofUnmarshaler(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error) { + m := msg.(*PolicyUpdateRequest) switch tag { case 1: // scope.global if wire != proto.WireVarint { return true, proto.ErrInternalBadWireType } x, err := b.DecodeVarint() - m.Scope = &FeeUpdateRequest_Global{x != 0} + m.Scope = &PolicyUpdateRequest_Global{x != 0} return true, err case 2: // scope.chan_point if wire != proto.WireBytes { @@ -3589,21 +3598,21 @@ func _FeeUpdateRequest_OneofUnmarshaler(msg proto.Message, tag, wire int, b *pro } msg := new(ChannelPoint) err := b.DecodeMessage(msg) - m.Scope = &FeeUpdateRequest_ChanPoint{msg} + m.Scope = &PolicyUpdateRequest_ChanPoint{msg} return true, err default: return false, nil } } -func _FeeUpdateRequest_OneofSizer(msg proto.Message) (n int) { - m := msg.(*FeeUpdateRequest) +func _PolicyUpdateRequest_OneofSizer(msg proto.Message) (n int) { + m := msg.(*PolicyUpdateRequest) // scope switch x := m.Scope.(type) { - case *FeeUpdateRequest_Global: + case *PolicyUpdateRequest_Global: n += proto.SizeVarint(1<<3 | proto.WireVarint) n += 1 - case *FeeUpdateRequest_ChanPoint: + case *PolicyUpdateRequest_ChanPoint: s := proto.Size(x.ChanPoint) n += proto.SizeVarint(2<<3 | proto.WireBytes) n += proto.SizeVarint(uint64(s)) @@ -3615,13 +3624,13 @@ func _FeeUpdateRequest_OneofSizer(msg proto.Message) (n int) { return n } -type FeeUpdateResponse struct { +type PolicyUpdateResponse struct { } -func (m *FeeUpdateResponse) Reset() { *m = FeeUpdateResponse{} } -func (m *FeeUpdateResponse) String() string { return proto.CompactTextString(m) } -func (*FeeUpdateResponse) ProtoMessage() {} -func (*FeeUpdateResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{91} } +func (m *PolicyUpdateResponse) Reset() { *m = PolicyUpdateResponse{} } +func (m *PolicyUpdateResponse) String() string { return proto.CompactTextString(m) } +func (*PolicyUpdateResponse) ProtoMessage() {} +func (*PolicyUpdateResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{91} } func init() { proto.RegisterType((*CreateWalletRequest)(nil), "lnrpc.CreateWalletRequest") @@ -3718,8 +3727,8 @@ func init() { proto.RegisterType((*FeeReportRequest)(nil), "lnrpc.FeeReportRequest") proto.RegisterType((*ChannelFeeReport)(nil), "lnrpc.ChannelFeeReport") proto.RegisterType((*FeeReportResponse)(nil), "lnrpc.FeeReportResponse") - proto.RegisterType((*FeeUpdateRequest)(nil), "lnrpc.FeeUpdateRequest") - proto.RegisterType((*FeeUpdateResponse)(nil), "lnrpc.FeeUpdateResponse") + proto.RegisterType((*PolicyUpdateRequest)(nil), "lnrpc.PolicyUpdateRequest") + proto.RegisterType((*PolicyUpdateResponse)(nil), "lnrpc.PolicyUpdateResponse") proto.RegisterEnum("lnrpc.NewAddressRequest_AddressType", NewAddressRequest_AddressType_name, NewAddressRequest_AddressType_value) } @@ -4033,10 +4042,10 @@ type LightningClient interface { // FeeReport allows the caller to obtain a report detailing the current fee // schedule enforced by the node globally for each channel. FeeReport(ctx context.Context, in *FeeReportRequest, opts ...grpc.CallOption) (*FeeReportResponse, error) - // * lncli: `updatefees` - // UpdateFees allows the caller to update the fee schedule for all channels - // globally, or a particular channel. - UpdateFees(ctx context.Context, in *FeeUpdateRequest, opts ...grpc.CallOption) (*FeeUpdateResponse, error) + // * lncli: `updatechanpolicy` + // UpdateChannelPolicy allows the caller to update the fee schedule and + // channel policies for all channels globally, or a particular channel. + UpdateChannelPolicy(ctx context.Context, in *PolicyUpdateRequest, opts ...grpc.CallOption) (*PolicyUpdateResponse, error) } type lightningClient struct { @@ -4517,9 +4526,9 @@ func (c *lightningClient) FeeReport(ctx context.Context, in *FeeReportRequest, o return out, nil } -func (c *lightningClient) UpdateFees(ctx context.Context, in *FeeUpdateRequest, opts ...grpc.CallOption) (*FeeUpdateResponse, error) { - out := new(FeeUpdateResponse) - err := grpc.Invoke(ctx, "/lnrpc.Lightning/UpdateFees", in, out, c.cc, opts...) +func (c *lightningClient) UpdateChannelPolicy(ctx context.Context, in *PolicyUpdateRequest, opts ...grpc.CallOption) (*PolicyUpdateResponse, error) { + out := new(PolicyUpdateResponse) + err := grpc.Invoke(ctx, "/lnrpc.Lightning/UpdateChannelPolicy", in, out, c.cc, opts...) if err != nil { return nil, err } @@ -4719,10 +4728,10 @@ type LightningServer interface { // FeeReport allows the caller to obtain a report detailing the current fee // schedule enforced by the node globally for each channel. FeeReport(context.Context, *FeeReportRequest) (*FeeReportResponse, error) - // * lncli: `updatefees` - // UpdateFees allows the caller to update the fee schedule for all channels - // globally, or a particular channel. - UpdateFees(context.Context, *FeeUpdateRequest) (*FeeUpdateResponse, error) + // * lncli: `updatechanpolicy` + // UpdateChannelPolicy allows the caller to update the fee schedule and + // channel policies for all channels globally, or a particular channel. + UpdateChannelPolicy(context.Context, *PolicyUpdateRequest) (*PolicyUpdateResponse, error) } func RegisterLightningServer(s *grpc.Server, srv LightningServer) { @@ -5418,20 +5427,20 @@ func _Lightning_FeeReport_Handler(srv interface{}, ctx context.Context, dec func return interceptor(ctx, in, info, handler) } -func _Lightning_UpdateFees_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(FeeUpdateRequest) +func _Lightning_UpdateChannelPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PolicyUpdateRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(LightningServer).UpdateFees(ctx, in) + return srv.(LightningServer).UpdateChannelPolicy(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/lnrpc.Lightning/UpdateFees", + FullMethod: "/lnrpc.Lightning/UpdateChannelPolicy", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(LightningServer).UpdateFees(ctx, req.(*FeeUpdateRequest)) + return srv.(LightningServer).UpdateChannelPolicy(ctx, req.(*PolicyUpdateRequest)) } return interceptor(ctx, in, info, handler) } @@ -5565,8 +5574,8 @@ var _Lightning_serviceDesc = grpc.ServiceDesc{ Handler: _Lightning_FeeReport_Handler, }, { - MethodName: "UpdateFees", - Handler: _Lightning_UpdateFees_Handler, + MethodName: "UpdateChannelPolicy", + Handler: _Lightning_UpdateChannelPolicy_Handler, }, }, Streams: []grpc.StreamDesc{ @@ -5608,309 +5617,310 @@ var _Lightning_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("rpc.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 4855 bytes of a gzipped FileDescriptorProto + // 4867 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x7b, 0xcd, 0x8f, 0x1c, 0x49, 0x56, 0xb8, 0xb3, 0xba, 0xfa, 0xa3, 0x5e, 0x55, 0xf5, 0x47, 0x74, 0xbb, 0xbb, 0x5c, 0xf6, 0x78, 0xed, 0xd8, 0xd1, 0x4c, 0xff, 0xfc, 0x1b, 0xdc, 0x76, 0x2f, 0x3b, 0xcc, 0x8e, 0x81, 0x91, 0xbf, 0x7b, 0xd8, 0x1e, 0x4f, 0x6f, 0xb6, 0x67, 0x06, 0x76, 0x85, 0x8a, 0xec, 0xaa, 0xe8, 0xea, 0x5c, 0x67, 0x65, 0xe6, 0x64, 0x46, 0x75, 0xbb, 0xd6, 0x58, 0x82, 0x05, 0x21, 0x21, 0x81, 0xf6, 0x00, - 0x02, 0xed, 0x61, 0xb9, 0x70, 0x81, 0x03, 0xfc, 0x03, 0x48, 0xfc, 0x01, 0x2b, 0x21, 0x90, 0xf6, - 0x84, 0xc4, 0x0d, 0x4e, 0xcb, 0x99, 0x3b, 0x7a, 0xf1, 0x95, 0x11, 0x99, 0xd9, 0xb6, 0x97, 0x5d, - 0xb8, 0x55, 0xbc, 0x78, 0xf9, 0x22, 0xe2, 0xc5, 0x8b, 0xf7, 0x5d, 0xd0, 0xca, 0xd2, 0xe1, 0xcd, - 0x34, 0x4b, 0x78, 0x42, 0xe6, 0xa3, 0x38, 0x4b, 0x87, 0xfd, 0x2b, 0xe3, 0x24, 0x19, 0x47, 0x6c, - 0x27, 0x48, 0xc3, 0x9d, 0x20, 0x8e, 0x13, 0x1e, 0xf0, 0x30, 0x89, 0x73, 0x89, 0x44, 0x6f, 0xc3, - 0xfa, 0xfd, 0x8c, 0x05, 0x9c, 0x7d, 0x11, 0x44, 0x11, 0xe3, 0x3e, 0xfb, 0x72, 0xca, 0x72, 0x4e, - 0xfa, 0xb0, 0x94, 0x06, 0x79, 0x7e, 0x96, 0x64, 0xa3, 0x9e, 0x77, 0xcd, 0xdb, 0xee, 0xf8, 0x66, - 0x4c, 0x37, 0x61, 0xc3, 0xfd, 0x24, 0x4f, 0x93, 0x38, 0x67, 0x48, 0xea, 0xb3, 0x38, 0x4a, 0x86, - 0xcf, 0x7e, 0x26, 0x52, 0xee, 0x27, 0x8a, 0xd4, 0x0f, 0x1b, 0xd0, 0x7e, 0x9a, 0x05, 0x71, 0x1e, - 0x0c, 0x71, 0xb3, 0xa4, 0x07, 0x8b, 0xfc, 0xf9, 0xe0, 0x24, 0xc8, 0x4f, 0x04, 0x89, 0x96, 0xaf, - 0x87, 0x64, 0x13, 0x16, 0x82, 0x49, 0x32, 0x8d, 0x79, 0xaf, 0x71, 0xcd, 0xdb, 0x9e, 0xf3, 0xd5, - 0x88, 0xbc, 0x07, 0x6b, 0xf1, 0x74, 0x32, 0x18, 0x26, 0xf1, 0x71, 0x98, 0x4d, 0xe4, 0x91, 0x7b, - 0x73, 0xd7, 0xbc, 0xed, 0x79, 0xbf, 0x3a, 0x41, 0xae, 0x02, 0x1c, 0xe1, 0x36, 0xe4, 0x12, 0x4d, - 0xb1, 0x84, 0x05, 0x21, 0x14, 0x3a, 0x6a, 0xc4, 0xc2, 0xf1, 0x09, 0xef, 0xcd, 0x0b, 0x42, 0x0e, - 0x0c, 0x69, 0xf0, 0x70, 0xc2, 0x06, 0x39, 0x0f, 0x26, 0x69, 0x6f, 0x41, 0xec, 0xc6, 0x82, 0x88, - 0xf9, 0x84, 0x07, 0xd1, 0xe0, 0x98, 0xb1, 0xbc, 0xb7, 0xa8, 0xe6, 0x0d, 0x84, 0xbc, 0x03, 0xcb, - 0x23, 0x96, 0xf3, 0x41, 0x30, 0x1a, 0x65, 0x2c, 0xcf, 0x59, 0xde, 0x5b, 0xba, 0x36, 0xb7, 0xdd, - 0xf2, 0x4b, 0x50, 0xda, 0x83, 0xcd, 0xc7, 0x8c, 0x5b, 0xdc, 0xc9, 0x15, 0xa7, 0xe9, 0x3e, 0x10, - 0x0b, 0xfc, 0x80, 0xf1, 0x20, 0x8c, 0x72, 0xf2, 0x3e, 0x74, 0xb8, 0x85, 0xdc, 0xf3, 0xae, 0xcd, - 0x6d, 0xb7, 0x77, 0xc9, 0x4d, 0x21, 0x1d, 0x37, 0xad, 0x0f, 0x7c, 0x07, 0x8f, 0xfe, 0x8b, 0x07, - 0xed, 0x43, 0x16, 0x8f, 0xf4, 0x3d, 0x12, 0x68, 0xe2, 0x4e, 0xd4, 0x1d, 0x8a, 0xdf, 0xe4, 0x2b, - 0xd0, 0x16, 0xbb, 0xcb, 0x79, 0x16, 0xc6, 0x63, 0x71, 0x05, 0x2d, 0x1f, 0x10, 0x74, 0x28, 0x20, - 0x64, 0x15, 0xe6, 0x82, 0x09, 0x17, 0x8c, 0x9f, 0xf3, 0xf1, 0x27, 0xb9, 0x0e, 0x9d, 0x34, 0x98, - 0x4d, 0x58, 0xcc, 0x0b, 0x66, 0x77, 0xfc, 0xb6, 0x82, 0xed, 0x21, 0xb7, 0x6f, 0xc2, 0xba, 0x8d, - 0xa2, 0xa9, 0xcf, 0x0b, 0xea, 0x6b, 0x16, 0xa6, 0x5a, 0xe4, 0x5d, 0x58, 0xd1, 0xf8, 0x99, 0xdc, - 0xac, 0x60, 0x7f, 0xcb, 0x5f, 0x56, 0x60, 0xcd, 0xa0, 0x3f, 0xf7, 0xa0, 0x23, 0x8f, 0x24, 0xe5, - 0x8c, 0xbc, 0x0d, 0x5d, 0xfd, 0x25, 0xcb, 0xb2, 0x24, 0x53, 0xd2, 0xe5, 0x02, 0xc9, 0x0d, 0x58, - 0xd5, 0x80, 0x34, 0x63, 0xe1, 0x24, 0x18, 0x33, 0x71, 0xd4, 0x8e, 0x5f, 0x81, 0x93, 0xdd, 0x82, - 0x62, 0x96, 0x4c, 0x39, 0x13, 0x47, 0x6f, 0xef, 0x76, 0x14, 0xbb, 0x7d, 0x84, 0xf9, 0x2e, 0x0a, - 0xfd, 0xbe, 0x07, 0x9d, 0xfb, 0x27, 0x41, 0x1c, 0xb3, 0xe8, 0x20, 0x09, 0x63, 0x8e, 0xe2, 0x76, - 0x3c, 0x8d, 0x47, 0x61, 0x3c, 0x1e, 0xf0, 0xe7, 0xa1, 0x7e, 0x36, 0x0e, 0x0c, 0x37, 0x65, 0x8f, - 0x91, 0x49, 0x8a, 0xff, 0x15, 0x38, 0xd2, 0x4b, 0xa6, 0x3c, 0x9d, 0xf2, 0x41, 0x18, 0x8f, 0xd8, - 0x73, 0xb1, 0xa7, 0xae, 0xef, 0xc0, 0xe8, 0xaf, 0xc3, 0xea, 0x3e, 0xca, 0x71, 0x1c, 0xc6, 0xe3, - 0xbb, 0x52, 0xd8, 0xf0, 0x71, 0xa5, 0xd3, 0xa3, 0x67, 0x6c, 0xa6, 0xf8, 0xa2, 0x46, 0x28, 0x0a, - 0x27, 0x49, 0xce, 0xd5, 0x7a, 0xe2, 0x37, 0xfd, 0x77, 0x0f, 0x56, 0x90, 0xb7, 0x9f, 0x04, 0xf1, - 0x4c, 0x8b, 0xcc, 0x3e, 0x74, 0x90, 0xd4, 0xd3, 0xe4, 0xae, 0x7c, 0xa2, 0x52, 0xf4, 0xb6, 0x15, - 0x2f, 0x4a, 0xd8, 0x37, 0x6d, 0xd4, 0x87, 0x31, 0xcf, 0x66, 0xbe, 0xf3, 0x35, 0x0a, 0x1b, 0x0f, - 0xb2, 0x31, 0xe3, 0xe2, 0xf1, 0xaa, 0xc7, 0x0c, 0x12, 0x74, 0x3f, 0x89, 0x8f, 0xc9, 0x35, 0xe8, - 0xe4, 0x01, 0x1f, 0xa4, 0x2c, 0x1b, 0x1c, 0xcd, 0x38, 0x13, 0x02, 0x33, 0xe7, 0x43, 0x1e, 0xf0, - 0x03, 0x96, 0xdd, 0x9b, 0x71, 0xd6, 0xff, 0x08, 0xd6, 0x2a, 0xab, 0xa0, 0x8c, 0x16, 0x47, 0xc4, - 0x9f, 0x64, 0x03, 0xe6, 0x4f, 0x83, 0x68, 0xca, 0x94, 0x4e, 0x91, 0x83, 0x0f, 0x1b, 0x1f, 0x78, - 0xf4, 0x1d, 0x58, 0x2d, 0xb6, 0xad, 0x84, 0x88, 0x40, 0xd3, 0xdc, 0x52, 0xcb, 0x17, 0xbf, 0xe9, - 0xef, 0x7b, 0x12, 0xf1, 0x7e, 0x12, 0x9a, 0xf7, 0x89, 0x88, 0xf8, 0x8c, 0x35, 0x22, 0xfe, 0x3e, - 0x57, 0x7f, 0xfd, 0xfc, 0x87, 0xa5, 0xef, 0xc2, 0x9a, 0xb5, 0x85, 0x57, 0x6c, 0xf6, 0xaf, 0x3c, - 0x58, 0x7b, 0xc2, 0xce, 0xd4, 0xad, 0xeb, 0xdd, 0x7e, 0x00, 0x4d, 0x3e, 0x4b, 0x99, 0xc0, 0x5c, - 0xde, 0x7d, 0x5b, 0x5d, 0x5a, 0x05, 0xef, 0xa6, 0x1a, 0x3e, 0x9d, 0xa5, 0xcc, 0x17, 0x5f, 0xd0, - 0x4f, 0xa1, 0x6d, 0x01, 0xc9, 0x16, 0xac, 0x7f, 0xf1, 0xf1, 0xd3, 0x27, 0x0f, 0x0f, 0x0f, 0x07, - 0x07, 0x9f, 0xdd, 0xfb, 0xe6, 0xc3, 0xdf, 0x1a, 0xec, 0xdd, 0x3d, 0xdc, 0x5b, 0xbd, 0x40, 0x36, - 0x81, 0x3c, 0x79, 0x78, 0xf8, 0xf4, 0xe1, 0x03, 0x07, 0xee, 0x91, 0x15, 0x68, 0xdb, 0x80, 0x06, - 0xed, 0x43, 0xef, 0x09, 0x3b, 0xfb, 0x22, 0xe4, 0x31, 0xcb, 0x73, 0x77, 0x79, 0x7a, 0x13, 0x88, - 0xbd, 0x27, 0x75, 0xcc, 0x1e, 0x2c, 0x2a, 0x8d, 0xa9, 0x0d, 0x86, 0x1a, 0xd2, 0x77, 0x80, 0x1c, - 0x86, 0xe3, 0xf8, 0x13, 0x96, 0xe7, 0xc1, 0x98, 0xe9, 0xc3, 0xae, 0xc2, 0xdc, 0x24, 0x1f, 0xab, - 0x87, 0x86, 0x3f, 0xe9, 0xd7, 0x60, 0xdd, 0xc1, 0x53, 0x84, 0xaf, 0x40, 0x2b, 0x0f, 0xc7, 0x71, - 0xc0, 0xa7, 0x19, 0x53, 0xa4, 0x0b, 0x00, 0x7d, 0x04, 0x1b, 0x9f, 0xb3, 0x2c, 0x3c, 0x9e, 0xbd, - 0x8e, 0xbc, 0x4b, 0xa7, 0x51, 0xa6, 0xf3, 0x10, 0x2e, 0x96, 0xe8, 0xa8, 0xe5, 0xa5, 0x64, 0xaa, - 0xfb, 0x5b, 0xf2, 0xe5, 0xc0, 0x7a, 0xa7, 0x0d, 0xfb, 0x9d, 0xd2, 0xcf, 0x80, 0xdc, 0x4f, 0xe2, - 0x98, 0x0d, 0xf9, 0x01, 0x63, 0x99, 0xde, 0xcc, 0xff, 0xb7, 0xc4, 0xb0, 0xbd, 0xbb, 0xa5, 0x2e, - 0xb6, 0xfc, 0xf8, 0x95, 0x7c, 0x12, 0x68, 0xa6, 0x2c, 0x9b, 0x08, 0xc2, 0x4b, 0xbe, 0xf8, 0x4d, - 0x77, 0x60, 0xdd, 0x21, 0x5b, 0xf0, 0x3c, 0x65, 0x2c, 0x1b, 0xa8, 0xdd, 0xcd, 0xfb, 0x7a, 0x48, - 0x6f, 0xc3, 0xc5, 0x07, 0x61, 0x3e, 0xac, 0x6e, 0x05, 0x3f, 0x99, 0x1e, 0x0d, 0x8a, 0xe7, 0xa7, - 0x87, 0x68, 0xe5, 0xca, 0x9f, 0x28, 0xdf, 0xe0, 0x8f, 0x3c, 0x68, 0xee, 0x3d, 0xdd, 0xbf, 0x8f, - 0x8e, 0x45, 0x18, 0x0f, 0x93, 0x09, 0xda, 0x06, 0xc9, 0x0e, 0x33, 0x3e, 0xf7, 0x59, 0x5d, 0x81, - 0x96, 0x30, 0x29, 0x68, 0xb8, 0xc5, 0xa3, 0xea, 0xf8, 0x05, 0x00, 0x9d, 0x06, 0xf6, 0x3c, 0x0d, - 0x33, 0xe1, 0x15, 0x68, 0x5b, 0xdf, 0x14, 0xca, 0xb2, 0x3a, 0x41, 0x7f, 0xda, 0x84, 0xee, 0xdd, - 0x21, 0x0f, 0x4f, 0x99, 0x52, 0xde, 0x62, 0x55, 0x01, 0x50, 0xfb, 0x51, 0x23, 0x34, 0x33, 0x19, - 0x9b, 0x24, 0x9c, 0x0d, 0x9c, 0x6b, 0x72, 0x81, 0x88, 0x35, 0x94, 0x84, 0x06, 0x29, 0x9a, 0x01, - 0xb1, 0xbf, 0x96, 0xef, 0x02, 0x91, 0x65, 0x08, 0x40, 0x2e, 0xe3, 0xce, 0x9a, 0xbe, 0x1e, 0x22, - 0x3f, 0x86, 0x41, 0x1a, 0x0c, 0x43, 0x3e, 0x53, 0xda, 0xc0, 0x8c, 0x91, 0x76, 0x94, 0x0c, 0x83, - 0x68, 0x70, 0x14, 0x44, 0x41, 0x3c, 0x64, 0xca, 0x3f, 0x71, 0x81, 0xe8, 0x82, 0xa8, 0x2d, 0x69, - 0x34, 0xe9, 0xa6, 0x94, 0xa0, 0xe8, 0xca, 0x0c, 0x93, 0xc9, 0x24, 0xe4, 0xe8, 0xb9, 0xf4, 0x96, - 0xa4, 0xe6, 0x29, 0x20, 0xe2, 0x24, 0x72, 0x74, 0x26, 0x79, 0xd8, 0x92, 0xab, 0x39, 0x40, 0xa4, - 0x72, 0xcc, 0x98, 0xd0, 0x60, 0xcf, 0xce, 0x7a, 0x20, 0xa9, 0x14, 0x10, 0xbc, 0x8d, 0x69, 0x9c, - 0x33, 0xce, 0x23, 0x36, 0x32, 0x1b, 0x6a, 0x0b, 0xb4, 0xea, 0x04, 0xb9, 0x05, 0xeb, 0xd2, 0x99, - 0xca, 0x03, 0x9e, 0xe4, 0x27, 0x61, 0x3e, 0xc8, 0x59, 0xcc, 0x7b, 0x1d, 0x81, 0x5f, 0x37, 0x45, - 0x3e, 0x80, 0xad, 0x12, 0x38, 0x63, 0x43, 0x16, 0x9e, 0xb2, 0x51, 0xaf, 0x2b, 0xbe, 0x3a, 0x6f, - 0x9a, 0x5c, 0x83, 0x36, 0xfa, 0x90, 0xd3, 0x74, 0x14, 0x70, 0x96, 0xf7, 0x96, 0xc5, 0x3d, 0xd8, - 0x20, 0x72, 0x1b, 0xba, 0x29, 0x93, 0x56, 0xf8, 0x84, 0x47, 0xc3, 0xbc, 0xb7, 0x22, 0x4c, 0x5f, - 0x5b, 0x3d, 0x36, 0x94, 0x5f, 0xdf, 0xc5, 0x40, 0xd1, 0x1c, 0xe6, 0xa7, 0x83, 0x11, 0x8b, 0x82, - 0x59, 0x6f, 0x55, 0x08, 0x5d, 0x01, 0xa0, 0x17, 0x61, 0x7d, 0x3f, 0xcc, 0xb9, 0x92, 0x34, 0xa3, - 0xfd, 0xf6, 0x60, 0xc3, 0x05, 0xab, 0xb7, 0x78, 0x0b, 0x96, 0x94, 0xd8, 0xe4, 0xbd, 0xb6, 0x58, - 0x7a, 0x43, 0x2d, 0xed, 0x48, 0xac, 0x6f, 0xb0, 0xe8, 0x1f, 0x36, 0xa0, 0x89, 0xef, 0xec, 0xfc, - 0x37, 0x69, 0x3f, 0xf0, 0x86, 0xf3, 0xc0, 0x6d, 0x75, 0x3b, 0xe7, 0xa8, 0x5b, 0xe1, 0x59, 0xcf, - 0x38, 0x53, 0xb7, 0x21, 0x25, 0xd6, 0x82, 0x14, 0xf3, 0x19, 0x1b, 0x9e, 0x0a, 0xb1, 0x35, 0xf3, - 0x08, 0x41, 0xa1, 0x46, 0x33, 0x27, 0xbe, 0x96, 0x32, 0x6b, 0xc6, 0x7a, 0x4e, 0x7c, 0xb9, 0x58, - 0xcc, 0x89, 0xef, 0x7a, 0xb0, 0x18, 0xc6, 0x47, 0xc9, 0x34, 0x1e, 0x09, 0xf9, 0x5c, 0xf2, 0xf5, - 0x10, 0xf9, 0x9c, 0x0a, 0xef, 0x28, 0x9c, 0x30, 0x25, 0x98, 0x05, 0x80, 0x12, 0x74, 0x83, 0x72, - 0xa1, 0x71, 0x0c, 0x93, 0xdf, 0x87, 0x35, 0x0b, 0xa6, 0x38, 0x7c, 0x1d, 0xe6, 0xf1, 0xf4, 0xda, - 0x9f, 0xd6, 0x37, 0x2b, 0x54, 0x95, 0x9c, 0xa1, 0xab, 0xb0, 0xfc, 0x98, 0xf1, 0x8f, 0xe3, 0xe3, - 0x44, 0x53, 0xfa, 0xe3, 0x39, 0x58, 0x31, 0x20, 0x45, 0x68, 0x1b, 0x56, 0xc2, 0x11, 0x8b, 0x79, - 0xc8, 0x67, 0x03, 0xc7, 0xdb, 0x2a, 0x83, 0x51, 0xf9, 0x07, 0x51, 0x18, 0xe4, 0x4a, 0x7d, 0xc8, - 0x01, 0xd9, 0x85, 0x0d, 0x94, 0x3c, 0x2d, 0x4c, 0xe6, 0xda, 0xa5, 0x93, 0x57, 0x3b, 0x87, 0x8f, - 0x05, 0xe1, 0x52, 0x3d, 0x15, 0x9f, 0x48, 0x55, 0x57, 0x37, 0x85, 0x5c, 0x93, 0x94, 0xf0, 0xc8, - 0xf3, 0x52, 0x3a, 0x0d, 0xa0, 0x12, 0x1f, 0x2d, 0x48, 0x07, 0xb3, 0x1c, 0x1f, 0x59, 0x31, 0xd6, - 0x52, 0x25, 0xc6, 0xda, 0x86, 0x95, 0x7c, 0x16, 0x0f, 0xd9, 0x68, 0xc0, 0x13, 0x5c, 0x37, 0x8c, - 0xc5, 0xed, 0x2c, 0xf9, 0x65, 0xb0, 0x88, 0x06, 0x59, 0xce, 0x63, 0xc6, 0x85, 0xd6, 0x58, 0xf2, - 0xf5, 0x10, 0x15, 0xb0, 0x40, 0x91, 0x42, 0xdf, 0xf2, 0xd5, 0x08, 0xad, 0xd8, 0x34, 0x0b, 0xf3, - 0x5e, 0x47, 0x40, 0xc5, 0x6f, 0xfa, 0x3d, 0x61, 0x1c, 0x4d, 0x10, 0xf8, 0x99, 0x78, 0xb9, 0xe4, - 0x32, 0xb4, 0xe4, 0x9e, 0xf2, 0x93, 0x40, 0x87, 0xab, 0x02, 0x70, 0x78, 0x12, 0x60, 0xec, 0xe2, - 0x1c, 0x53, 0xbe, 0x82, 0xb6, 0x80, 0xed, 0xc9, 0x53, 0xbe, 0x0d, 0xcb, 0x3a, 0xbc, 0xcc, 0x07, - 0x11, 0x3b, 0xe6, 0xda, 0xd9, 0x8e, 0xa7, 0x13, 0x5c, 0x2e, 0xdf, 0x67, 0xc7, 0x9c, 0x3e, 0x81, - 0x35, 0xf5, 0x02, 0x3f, 0x4d, 0x99, 0x5e, 0xfa, 0x1b, 0x65, 0xfd, 0x2f, 0x0d, 0xf4, 0xba, 0x92, - 0x2c, 0x3b, 0x42, 0x28, 0x19, 0x05, 0xea, 0x03, 0x51, 0xd3, 0xf7, 0xa3, 0x24, 0x67, 0x8a, 0x20, - 0x85, 0xce, 0x30, 0x4a, 0xf2, 0x72, 0x18, 0x61, 0xc3, 0x90, 0x97, 0xf9, 0x74, 0x38, 0xc4, 0x97, - 0x2b, 0x4d, 0xbc, 0x1e, 0xd2, 0xbf, 0xf1, 0x60, 0x5d, 0x50, 0xd3, 0xba, 0xc2, 0xf8, 0x85, 0x6f, - 0xbe, 0xcd, 0xce, 0xd0, 0x0e, 0x6b, 0x36, 0x60, 0xfe, 0x38, 0xc9, 0x86, 0x4c, 0xad, 0x24, 0x07, - 0xbf, 0x08, 0x4f, 0xf7, 0x5f, 0x3d, 0x58, 0x13, 0x5b, 0x3d, 0xe4, 0x01, 0x9f, 0xe6, 0xea, 0xf8, - 0xbf, 0x0a, 0x5d, 0x3c, 0x2a, 0xd3, 0xe2, 0xaf, 0x36, 0xba, 0x61, 0x5e, 0xaa, 0x80, 0x4a, 0xe4, - 0xbd, 0x0b, 0xbe, 0x8b, 0x4c, 0x3e, 0x82, 0x8e, 0x9d, 0x23, 0x10, 0x7b, 0x6e, 0xef, 0x5e, 0xd2, - 0xa7, 0xac, 0x48, 0xce, 0xde, 0x05, 0xdf, 0xf9, 0x80, 0xdc, 0x01, 0x10, 0x96, 0x59, 0x90, 0x55, - 0x61, 0xe0, 0x25, 0x97, 0x49, 0xd6, 0x65, 0xed, 0x5d, 0xf0, 0x2d, 0xf4, 0x7b, 0x4b, 0xb0, 0x20, - 0x4d, 0x09, 0x7d, 0x0c, 0x5d, 0x67, 0xa7, 0x8e, 0x07, 0xdf, 0x91, 0x1e, 0x7c, 0x25, 0xc0, 0x6b, - 0xd4, 0x04, 0x78, 0xff, 0xd0, 0x00, 0x82, 0xd2, 0x56, 0xba, 0xce, 0x77, 0x60, 0x59, 0xb1, 0xdf, - 0x75, 0xde, 0x4a, 0x50, 0x61, 0xf3, 0x92, 0x91, 0xe3, 0xc1, 0x74, 0x7c, 0x1b, 0x44, 0x6e, 0x02, - 0xb1, 0x86, 0x3a, 0x6a, 0x97, 0xf6, 0xa0, 0x66, 0x06, 0x15, 0x97, 0x74, 0x3f, 0x74, 0xbc, 0xaa, - 0x3c, 0xb6, 0xa6, 0xb8, 0xdf, 0xda, 0x39, 0x91, 0x4c, 0x9a, 0xe6, 0x27, 0x68, 0x93, 0xb5, 0x8f, - 0xa3, 0xc7, 0x65, 0x41, 0x5a, 0x78, 0xad, 0x20, 0x2d, 0x96, 0x05, 0x49, 0x58, 0xb8, 0x2c, 0x3c, - 0x0d, 0x38, 0xd3, 0x56, 0x43, 0x0d, 0xe9, 0x4f, 0x3c, 0x58, 0x45, 0xee, 0x39, 0x12, 0xf6, 0x21, - 0x08, 0x01, 0x7f, 0x43, 0x01, 0x73, 0x70, 0x7f, 0x7e, 0xf9, 0xfa, 0x00, 0x5a, 0x82, 0x60, 0x92, - 0xb2, 0x58, 0x89, 0x57, 0xcf, 0x15, 0xaf, 0x42, 0xb7, 0xec, 0x5d, 0xf0, 0x0b, 0x64, 0x4b, 0xb8, - 0xfe, 0xd9, 0x83, 0xb6, 0xda, 0xe6, 0xff, 0xd8, 0xa5, 0xee, 0xc3, 0x12, 0xca, 0x99, 0xe5, 0xb1, - 0x9a, 0x31, 0xea, 0xf4, 0x09, 0x46, 0x34, 0x68, 0xc4, 0x1c, 0x77, 0xba, 0x0c, 0x46, 0x8b, 0x24, - 0xd4, 0x68, 0x3e, 0xe0, 0x61, 0x34, 0xd0, 0xb3, 0x2a, 0xd1, 0x56, 0x37, 0x85, 0xda, 0x24, 0xe7, - 0xc1, 0x98, 0x29, 0x63, 0x23, 0x07, 0x18, 0x37, 0xa8, 0x03, 0x95, 0x5d, 0xa5, 0x1f, 0x03, 0x6c, - 0x55, 0xa6, 0x8c, 0xbb, 0xa4, 0x3c, 0xc4, 0x28, 0x9c, 0x1c, 0x25, 0xc6, 0xd9, 0xf4, 0x6c, 0xe7, - 0xd1, 0x99, 0x22, 0x63, 0xb8, 0xa8, 0xad, 0x2a, 0xf2, 0xb4, 0xb0, 0xa1, 0x0d, 0xe1, 0x0e, 0xdc, - 0x76, 0x65, 0xa0, 0xbc, 0xa0, 0x86, 0xdb, 0xef, 0xb1, 0x9e, 0x1e, 0x39, 0x81, 0x9e, 0x31, 0xdf, - 0x4a, 0x71, 0x5b, 0x26, 0x1e, 0xd7, 0x7a, 0xef, 0x35, 0x6b, 0x09, 0x2d, 0x33, 0xd2, 0xcb, 0x9c, - 0x4b, 0x8d, 0xcc, 0xe0, 0xaa, 0x9e, 0x13, 0x9a, 0xb9, 0xba, 0x5e, 0xf3, 0x8d, 0xce, 0xf6, 0x08, - 0x3f, 0x76, 0x17, 0x7d, 0x0d, 0xe1, 0xfe, 0x8f, 0x3d, 0x58, 0x76, 0xc9, 0xa1, 0xe8, 0xa8, 0xa8, - 0x43, 0xab, 0x0e, 0xed, 0x16, 0x95, 0xc0, 0xd5, 0xb8, 0xa9, 0x51, 0x17, 0x37, 0xd9, 0xd1, 0xd1, - 0xdc, 0xeb, 0xa2, 0xa3, 0xe6, 0x9b, 0x45, 0x47, 0xf3, 0x75, 0xd1, 0x51, 0xff, 0xbf, 0x3c, 0x20, - 0xd5, 0xfb, 0x25, 0x8f, 0x65, 0xe0, 0x16, 0xb3, 0x48, 0xe9, 0x89, 0x5f, 0x7a, 0x33, 0x19, 0xd1, - 0x3c, 0xd4, 0x5f, 0xa3, 0xb0, 0xda, 0x8a, 0xc0, 0x76, 0x46, 0xba, 0x7e, 0xdd, 0x54, 0x29, 0x5e, - 0x6b, 0xbe, 0x3e, 0x5e, 0x9b, 0x7f, 0x7d, 0xbc, 0xb6, 0x50, 0x8e, 0xd7, 0xfa, 0xbf, 0x0b, 0x5d, - 0xe7, 0xd6, 0x7f, 0x71, 0x27, 0x2e, 0x3b, 0x32, 0xf2, 0x82, 0x1d, 0x58, 0xff, 0x3f, 0x1b, 0x40, - 0xaa, 0x92, 0xf7, 0x7f, 0xba, 0x07, 0x21, 0x47, 0x8e, 0x02, 0x99, 0x53, 0x72, 0xe4, 0xa8, 0x8e, - 0xff, 0x4d, 0xa5, 0xf8, 0x1e, 0xac, 0x65, 0x6c, 0x98, 0x9c, 0xb2, 0xcc, 0x8a, 0x99, 0xe5, 0x55, - 0x55, 0x27, 0xd0, 0x95, 0x73, 0xa3, 0xd4, 0x25, 0xa7, 0x36, 0x60, 0x59, 0x86, 0x52, 0xb0, 0x4a, - 0xbf, 0x01, 0x1b, 0xb2, 0x64, 0x73, 0x4f, 0x92, 0xd2, 0xde, 0xc4, 0x75, 0xe8, 0x9c, 0xc9, 0x34, - 0xdd, 0x20, 0x89, 0xa3, 0x99, 0x32, 0x22, 0x6d, 0x05, 0xfb, 0x34, 0x8e, 0x66, 0xf4, 0x47, 0x1e, - 0x5c, 0x2c, 0x7d, 0x5b, 0x64, 0xe3, 0xa5, 0xaa, 0x75, 0xf5, 0xaf, 0x0b, 0xc4, 0x23, 0x2a, 0x19, - 0xb7, 0x8e, 0x28, 0x4d, 0x52, 0x75, 0x02, 0x59, 0x38, 0x8d, 0xab, 0xf8, 0xf2, 0x62, 0xea, 0xa6, - 0xe8, 0x16, 0x5c, 0x54, 0x97, 0xef, 0x9e, 0x8d, 0xee, 0xc2, 0x66, 0x79, 0xa2, 0xc8, 0x7c, 0xb9, - 0x5b, 0xd6, 0x43, 0xfa, 0x11, 0x90, 0x6f, 0x4d, 0x59, 0x36, 0x13, 0x79, 0x7f, 0x93, 0x5a, 0xdd, - 0x2a, 0x87, 0xd8, 0x0b, 0xe9, 0xf4, 0xe8, 0x9b, 0x6c, 0xa6, 0xcb, 0x25, 0x0d, 0x53, 0x2e, 0xa1, - 0x77, 0x60, 0xdd, 0x21, 0x60, 0x58, 0xb5, 0x20, 0x6a, 0x07, 0x3a, 0xfc, 0x74, 0xeb, 0x0b, 0x6a, - 0x8e, 0xfe, 0xa5, 0x07, 0x73, 0x7b, 0x49, 0x6a, 0xe7, 0x8c, 0x3c, 0x37, 0x67, 0xa4, 0x74, 0xe7, - 0xc0, 0xa8, 0xc6, 0x86, 0x7a, 0xf9, 0x36, 0x10, 0x35, 0x5f, 0x30, 0xe1, 0x18, 0x80, 0x1d, 0x27, - 0xd9, 0x59, 0x90, 0x8d, 0x14, 0xff, 0x4a, 0x50, 0xdc, 0x7e, 0xa1, 0x60, 0xf0, 0x27, 0x3a, 0x0d, - 0x22, 0x71, 0x36, 0x53, 0x31, 0xa3, 0x1a, 0xd1, 0x1f, 0x78, 0x30, 0x2f, 0xf6, 0x8a, 0xaf, 0x41, - 0xde, 0xaf, 0x28, 0x95, 0x89, 0xbc, 0x9c, 0x27, 0x5f, 0x43, 0x09, 0x5c, 0x2a, 0xa0, 0x35, 0x2a, - 0x05, 0xb4, 0x2b, 0xd0, 0x92, 0xa3, 0xa2, 0xe2, 0x54, 0x00, 0xc8, 0x55, 0x68, 0x9e, 0x24, 0xa9, - 0xb6, 0x61, 0xa0, 0x13, 0x31, 0x49, 0xea, 0x0b, 0x38, 0xbd, 0x01, 0x2b, 0x4f, 0x92, 0x11, 0xb3, - 0xa2, 0xf5, 0x73, 0xaf, 0x89, 0xfe, 0x9e, 0x07, 0x4b, 0x1a, 0x99, 0x6c, 0x43, 0x13, 0x4d, 0x51, - 0xc9, 0xf9, 0x33, 0xe9, 0x54, 0xc4, 0xf3, 0x05, 0x06, 0xaa, 0x10, 0x11, 0x1b, 0x16, 0xae, 0x82, - 0x8e, 0x0c, 0x0b, 0x23, 0x8c, 0xee, 0xb8, 0xd8, 0x73, 0xc9, 0x58, 0x95, 0xa0, 0xf4, 0x6f, 0x3d, - 0xe8, 0x3a, 0x6b, 0xa0, 0x83, 0x1e, 0x05, 0x39, 0x57, 0x29, 0x28, 0xc5, 0x44, 0x1b, 0x64, 0x67, - 0x76, 0x1a, 0x6e, 0x66, 0xc7, 0x64, 0x16, 0xe6, 0xec, 0xcc, 0xc2, 0x2d, 0x68, 0x15, 0xc5, 0xc8, - 0xa6, 0xa3, 0x1a, 0x70, 0x45, 0x9d, 0x28, 0x2e, 0x90, 0x90, 0xce, 0x30, 0x89, 0x92, 0x4c, 0xd5, - 0xea, 0xe4, 0x80, 0xde, 0x81, 0xb6, 0x85, 0x8f, 0xdb, 0x88, 0x19, 0x3f, 0x4b, 0xb2, 0x67, 0x3a, - 0xc1, 0xa4, 0x86, 0xa6, 0x40, 0xd2, 0x28, 0x0a, 0x24, 0xf4, 0xef, 0x3c, 0xe8, 0xa2, 0xa4, 0x84, - 0xf1, 0xf8, 0x20, 0x89, 0xc2, 0xe1, 0x4c, 0x48, 0x8c, 0x16, 0x8a, 0xc1, 0x88, 0x45, 0x3c, 0x30, - 0x12, 0xe3, 0x82, 0xd1, 0xe6, 0x4f, 0xc2, 0x58, 0xa8, 0x2c, 0x25, 0x2f, 0x66, 0x8c, 0x92, 0x8f, - 0xb6, 0xeb, 0x28, 0xc8, 0xd9, 0x60, 0x82, 0xe1, 0x84, 0xd2, 0xd5, 0x0e, 0x10, 0xd5, 0x07, 0x02, - 0xb2, 0x80, 0xb3, 0xc1, 0x24, 0x8c, 0xa2, 0x50, 0xe2, 0x4a, 0x09, 0xaf, 0x9b, 0xc2, 0x30, 0xab, - 0xad, 0xd4, 0xc4, 0xc3, 0xd1, 0x58, 0xe6, 0x4a, 0x95, 0x23, 0x62, 0x9e, 0x9f, 0x05, 0xd1, 0xf3, - 0x8e, 0xeb, 0x62, 0x41, 0xca, 0xd7, 0x3a, 0x57, 0xbd, 0xd6, 0x2b, 0xd0, 0x42, 0xf1, 0xba, 0x2d, - 0x7c, 0x24, 0x59, 0xbb, 0x2e, 0x00, 0x7a, 0x76, 0x57, 0xcc, 0xce, 0x17, 0xb3, 0x02, 0xe0, 0x78, - 0x45, 0x0b, 0x25, 0xaf, 0xe8, 0x03, 0xe8, 0x28, 0x32, 0x82, 0xef, 0x22, 0x5c, 0x2a, 0x04, 0xdc, - 0xb9, 0x13, 0xdf, 0xc1, 0xd4, 0x5f, 0xee, 0xea, 0x2f, 0x97, 0x5e, 0xf7, 0xa5, 0xc6, 0xa4, 0x17, - 0x61, 0x5d, 0x31, 0xef, 0x71, 0x16, 0xa4, 0x27, 0x5a, 0xf5, 0x8e, 0x4c, 0x81, 0x54, 0x80, 0xc9, - 0x0d, 0x98, 0xc7, 0xcf, 0xb4, 0xf6, 0xab, 0x7f, 0x74, 0x12, 0x85, 0x6c, 0xc3, 0x3c, 0x1b, 0x8d, - 0x99, 0xf6, 0xcc, 0x89, 0x1b, 0x23, 0xe1, 0x1d, 0xf9, 0x12, 0x01, 0x55, 0x00, 0x42, 0x4b, 0x2a, - 0xc0, 0xd5, 0x9c, 0x0b, 0x38, 0xfc, 0x78, 0x44, 0x37, 0x80, 0x3c, 0x91, 0x52, 0x6b, 0xe7, 0xf7, - 0xfe, 0x60, 0x0e, 0xda, 0x16, 0x18, 0x5f, 0xf3, 0x18, 0x37, 0x3c, 0x18, 0x85, 0xc1, 0x84, 0x71, - 0x96, 0x29, 0x49, 0x2d, 0x41, 0x85, 0x82, 0x3d, 0x1d, 0x0f, 0x92, 0x29, 0x1f, 0x8c, 0xd8, 0x38, - 0x63, 0xd2, 0xa0, 0x79, 0x7e, 0x09, 0x8a, 0x78, 0x93, 0xe0, 0xb9, 0x8d, 0x27, 0xe5, 0xa1, 0x04, - 0xd5, 0xd9, 0x3a, 0xc9, 0xa3, 0x66, 0x91, 0xad, 0x93, 0x1c, 0x29, 0xeb, 0xa1, 0xf9, 0x1a, 0x3d, - 0xf4, 0x3e, 0x6c, 0x4a, 0x8d, 0xa3, 0xde, 0xe6, 0xa0, 0x24, 0x26, 0xe7, 0xcc, 0x92, 0x1b, 0xb0, - 0x8a, 0x7b, 0xd6, 0x02, 0x9e, 0x87, 0xdf, 0x93, 0x71, 0xb6, 0xe7, 0x57, 0xe0, 0x88, 0x8b, 0xcf, - 0xd1, 0xc1, 0x95, 0xc5, 0x84, 0x0a, 0x5c, 0xe0, 0x06, 0xcf, 0x5d, 0xdc, 0x96, 0xc2, 0x2d, 0xc1, - 0x69, 0x17, 0xda, 0x87, 0x3c, 0x49, 0xf5, 0xa5, 0x2c, 0x43, 0x47, 0x0e, 0x55, 0x01, 0xe9, 0x32, - 0x5c, 0x12, 0x52, 0xf4, 0x34, 0x49, 0x93, 0x28, 0x19, 0xcf, 0x0e, 0xa7, 0x47, 0xf9, 0x30, 0x0b, - 0x53, 0xf4, 0x98, 0xe9, 0x3f, 0x79, 0xb0, 0xee, 0xcc, 0xaa, 0x50, 0xff, 0x97, 0xa5, 0x48, 0x9b, - 0x9c, 0xbf, 0x14, 0xbc, 0x35, 0x4b, 0x1d, 0x4a, 0x44, 0x99, 0x12, 0xf9, 0x4c, 0x95, 0x01, 0xee, - 0xc2, 0x8a, 0xde, 0x99, 0xfe, 0x50, 0x4a, 0x61, 0xaf, 0x2a, 0x85, 0xea, 0xfb, 0x65, 0xf5, 0x81, - 0x26, 0xf1, 0x6b, 0xd2, 0xef, 0x64, 0x23, 0x71, 0x46, 0x1d, 0xf3, 0xf5, 0xf5, 0xf7, 0xb6, 0xb3, - 0xab, 0x77, 0x30, 0x34, 0xc0, 0x9c, 0xfe, 0x89, 0x07, 0x50, 0xec, 0x0e, 0x05, 0xa3, 0x50, 0xe9, - 0x9e, 0xc8, 0x86, 0x5a, 0xea, 0xfb, 0x3a, 0x74, 0x4c, 0xce, 0xb9, 0xb0, 0x12, 0x6d, 0x0d, 0x43, - 0x0f, 0xe5, 0x5d, 0x58, 0x19, 0x47, 0xc9, 0x91, 0xb0, 0xb9, 0xa2, 0x56, 0x99, 0xab, 0x32, 0xda, - 0xb2, 0x04, 0x3f, 0x52, 0xd0, 0xc2, 0xa4, 0x34, 0x2d, 0x93, 0x42, 0xff, 0xb4, 0x61, 0x32, 0x9f, - 0xc5, 0x99, 0xcf, 0x7d, 0x65, 0x64, 0xb7, 0xa2, 0x1c, 0xcf, 0x49, 0x34, 0x8a, 0xec, 0xc6, 0xc1, - 0x6b, 0x03, 0xbd, 0x3b, 0xb0, 0x9c, 0x49, 0xed, 0xa3, 0x55, 0x53, 0xf3, 0x15, 0xaa, 0xa9, 0x9b, - 0x39, 0x76, 0xe7, 0xff, 0xc1, 0x6a, 0x30, 0x3a, 0x65, 0x19, 0x0f, 0x85, 0xc7, 0x2f, 0x8c, 0xbe, - 0x54, 0xa8, 0x2b, 0x16, 0x5c, 0xd8, 0xe2, 0x77, 0x61, 0x45, 0x95, 0x2e, 0x0d, 0xa6, 0xea, 0x48, - 0x29, 0xc0, 0x88, 0x48, 0xff, 0x5a, 0x27, 0x59, 0xdd, 0x3b, 0x3c, 0x9f, 0x23, 0xf6, 0xe9, 0x1a, - 0xa5, 0xd3, 0x7d, 0x55, 0x25, 0x3c, 0x47, 0x3a, 0xac, 0x50, 0xa9, 0x67, 0x09, 0x54, 0x09, 0x6a, - 0x97, 0xa5, 0xcd, 0x37, 0x61, 0x29, 0xfd, 0xd1, 0x1c, 0x2c, 0x7e, 0x1c, 0x9f, 0x26, 0xe1, 0x50, - 0xa4, 0x1f, 0x27, 0x6c, 0x92, 0xe8, 0x06, 0x02, 0xfc, 0x8d, 0x16, 0x5d, 0xd4, 0xc6, 0x52, 0xae, - 0xf2, 0x82, 0x7a, 0x88, 0xd6, 0x2d, 0x2b, 0x9a, 0x66, 0xa4, 0xa4, 0x58, 0x10, 0xf4, 0x0f, 0x33, - 0xbb, 0x0f, 0x48, 0x8d, 0x8a, 0x0e, 0x8c, 0x79, 0xab, 0x03, 0x43, 0x24, 0xab, 0x65, 0xd9, 0x4f, - 0xb0, 0x73, 0xc9, 0xd7, 0x43, 0xe1, 0xc7, 0x66, 0x4c, 0x06, 0xbd, 0xc2, 0x4e, 0x2e, 0x2a, 0x3f, - 0xd6, 0x06, 0xa2, 0x2d, 0x95, 0x1f, 0x48, 0x1c, 0xa9, 0x6b, 0x6c, 0x10, 0xfa, 0x16, 0xe5, 0x56, - 0xa2, 0x96, 0xbc, 0xe2, 0x12, 0x18, 0x15, 0xd2, 0x88, 0x19, 0xbd, 0x21, 0xcf, 0x00, 0xb2, 0x29, - 0xa8, 0x0c, 0xb7, 0xbc, 0x60, 0x59, 0xbe, 0x54, 0x23, 0xe1, 0x83, 0x04, 0x51, 0x74, 0x14, 0x0c, - 0x9f, 0x89, 0x06, 0x2f, 0x51, 0xad, 0x6c, 0xf9, 0x2e, 0x10, 0x77, 0x3d, 0x8c, 0xf8, 0xe9, 0x40, - 0x91, 0xe8, 0xca, 0x6a, 0xa3, 0x05, 0xa2, 0x9f, 0x03, 0xb9, 0x3b, 0x1a, 0xa9, 0x1b, 0x32, 0x31, - 0x42, 0xc1, 0x5b, 0xcf, 0xe1, 0x6d, 0xcd, 0x19, 0x1b, 0xb5, 0x67, 0xa4, 0x0f, 0xa1, 0x7d, 0x60, - 0xf5, 0x65, 0x89, 0xcb, 0xd4, 0x1d, 0x59, 0x4a, 0x00, 0x2c, 0x88, 0xb5, 0x60, 0xc3, 0x5e, 0x90, - 0xfe, 0x0a, 0x90, 0xfd, 0x30, 0xe7, 0x66, 0x7f, 0x26, 0x54, 0x34, 0x19, 0x2f, 0x2b, 0x54, 0x54, - 0x30, 0x11, 0x2a, 0xde, 0x95, 0x45, 0xcf, 0xf2, 0xc1, 0x6e, 0xc0, 0x52, 0x28, 0x41, 0x5a, 0x0f, - 0x2f, 0x2b, 0x01, 0xd6, 0x98, 0x66, 0x1e, 0x1d, 0x0a, 0x05, 0x74, 0xd4, 0xfc, 0x0f, 0x3c, 0x58, - 0x54, 0x47, 0x43, 0x73, 0xe8, 0x74, 0xa4, 0xc9, 0x83, 0x39, 0xb0, 0xfa, 0x8e, 0xa0, 0xaa, 0xd4, - 0xcd, 0xd5, 0x49, 0x1d, 0x81, 0x66, 0x1a, 0xf0, 0x13, 0xe1, 0x41, 0xb7, 0x7c, 0xf1, 0x5b, 0x47, - 0x4a, 0xf3, 0x26, 0x52, 0xd2, 0x05, 0x5e, 0xb5, 0x29, 0x93, 0xb5, 0xbc, 0x27, 0x0b, 0xbc, 0x05, - 0xb8, 0xe0, 0x81, 0xda, 0x60, 0x99, 0x07, 0x0a, 0xd5, 0x37, 0xf3, 0xb4, 0x0f, 0xbd, 0x07, 0x2c, - 0x62, 0x9c, 0xdd, 0x8d, 0xa2, 0x32, 0xfd, 0xcb, 0x70, 0xa9, 0x66, 0x4e, 0x59, 0xca, 0x47, 0xb0, - 0xf6, 0x80, 0x1d, 0x4d, 0xc7, 0xfb, 0xec, 0xb4, 0x28, 0x18, 0x10, 0x68, 0xe6, 0x27, 0xc9, 0x99, - 0xba, 0x2f, 0xf1, 0x9b, 0xbc, 0x05, 0x10, 0x21, 0xce, 0x20, 0x4f, 0xd9, 0x50, 0xb7, 0xb3, 0x08, - 0xc8, 0x61, 0xca, 0x86, 0xf4, 0x7d, 0x20, 0x36, 0x1d, 0x75, 0x04, 0x7c, 0x8d, 0xd3, 0xa3, 0x41, - 0x3e, 0xcb, 0x39, 0x9b, 0xe8, 0x3e, 0x1d, 0x1b, 0x44, 0xdf, 0x85, 0xce, 0x41, 0x30, 0xf3, 0xd9, - 0x97, 0xaa, 0xd1, 0x0f, 0x03, 0xb2, 0x60, 0x86, 0xe2, 0x69, 0x02, 0x32, 0x31, 0x4d, 0xff, 0xb1, - 0x01, 0x0b, 0x12, 0x13, 0xa9, 0x8e, 0x58, 0xce, 0xc3, 0x58, 0xa6, 0xd5, 0x15, 0x55, 0x0b, 0x54, - 0xb9, 0xef, 0x46, 0xcd, 0x7d, 0x2b, 0x17, 0x49, 0x97, 0xfe, 0xd5, 0xc5, 0x3a, 0x30, 0x11, 0x6f, - 0x86, 0x13, 0x26, 0xfb, 0x3d, 0x9b, 0x2a, 0xde, 0xd4, 0x80, 0x52, 0xe4, 0x5b, 0xbc, 0x79, 0xb9, - 0x3f, 0x2d, 0x88, 0xca, 0x2c, 0xd8, 0xa0, 0x5a, 0xcd, 0xb2, 0x28, 0x3b, 0xfb, 0x2a, 0x9a, 0xa5, - 0xa2, 0x41, 0x96, 0xde, 0x40, 0x83, 0x48, 0xbf, 0xc9, 0xd1, 0x20, 0x04, 0x56, 0x1f, 0x31, 0xe6, - 0xb3, 0x34, 0xc9, 0x4c, 0xb7, 0xe4, 0x0f, 0x3d, 0x58, 0x55, 0x16, 0xc1, 0xcc, 0x91, 0xeb, 0x8e, - 0xf9, 0xf0, 0xea, 0x32, 0xad, 0x6f, 0x43, 0x57, 0x04, 0x50, 0x18, 0x1d, 0x89, 0x68, 0x49, 0xe5, - 0x14, 0x1c, 0x20, 0xee, 0x49, 0xe7, 0x0e, 0x27, 0x61, 0xa4, 0x18, 0x6c, 0x83, 0xd0, 0xd4, 0xe9, - 0x00, 0x4b, 0xb0, 0xd7, 0xf3, 0xcd, 0x98, 0x1e, 0xc0, 0x9a, 0xb5, 0x5f, 0x25, 0x50, 0x77, 0x40, - 0xd7, 0x1b, 0x65, 0x8a, 0x40, 0xbe, 0x8b, 0x2d, 0xd7, 0xb8, 0x15, 0x9f, 0x39, 0xc8, 0xf4, 0xef, - 0x3d, 0xc1, 0x02, 0xe5, 0x43, 0x99, 0xfe, 0xa4, 0x05, 0xe9, 0xd6, 0x48, 0x69, 0xdf, 0xbb, 0xe0, - 0xab, 0x31, 0xf9, 0xfa, 0x1b, 0x7a, 0x26, 0xa6, 0xae, 0x77, 0x0e, 0x6f, 0xe6, 0xea, 0x78, 0xf3, - 0x8a, 0x93, 0xdf, 0x5b, 0x84, 0xf9, 0x7c, 0x98, 0xa4, 0x8c, 0xae, 0x0b, 0x16, 0xe8, 0xfd, 0x4a, - 0x16, 0xec, 0xfe, 0x9b, 0x07, 0xcb, 0x32, 0xb9, 0x26, 0xfb, 0xaa, 0x59, 0x46, 0x30, 0x76, 0xb2, - 0xda, 0xb5, 0x89, 0x71, 0x1d, 0xab, 0x6d, 0xdf, 0xfd, 0xcb, 0xb5, 0x73, 0xda, 0x6f, 0xfe, 0xfe, - 0x4f, 0xfe, 0xe3, 0xcf, 0x1a, 0x17, 0xe9, 0xea, 0xce, 0xe9, 0xed, 0x1d, 0xa1, 0xe2, 0xd8, 0x99, - 0xc0, 0xf8, 0xd0, 0xbb, 0x81, 0xab, 0xd8, 0x9d, 0xdc, 0x66, 0x95, 0x9a, 0x8e, 0x70, 0xb3, 0x4a, - 0x6d, 0xeb, 0xb7, 0xb3, 0xca, 0x54, 0x60, 0x98, 0x55, 0x76, 0x7f, 0xda, 0x87, 0x96, 0x09, 0xf2, - 0xc8, 0x77, 0xa1, 0xeb, 0x24, 0x12, 0x89, 0x26, 0x5c, 0x97, 0x9a, 0xec, 0x5f, 0xa9, 0x9f, 0x54, - 0xcb, 0x5e, 0x15, 0xcb, 0xf6, 0xc8, 0x26, 0x2e, 0xab, 0xb2, 0x77, 0x3b, 0x22, 0xc3, 0x2a, 0x7b, - 0x0e, 0x9e, 0xc1, 0xb2, 0x9b, 0xfc, 0x23, 0x57, 0xdc, 0xdb, 0x2e, 0xad, 0xf6, 0xd6, 0x39, 0xb3, - 0x6a, 0xb9, 0x2b, 0x62, 0xb9, 0x4d, 0xb2, 0x61, 0x2f, 0x67, 0x82, 0x2f, 0x26, 0xba, 0x44, 0xec, - 0x16, 0x6f, 0xa2, 0xe9, 0xd5, 0xb7, 0x7e, 0xf7, 0x2f, 0x55, 0xdb, 0xb9, 0x55, 0xff, 0x37, 0xed, - 0x89, 0xa5, 0x08, 0x11, 0x0c, 0xb5, 0x3b, 0xbc, 0xc9, 0x77, 0xa0, 0x65, 0x1a, 0x44, 0xc9, 0x96, - 0xd5, 0x95, 0x6b, 0x77, 0xad, 0xf6, 0x7b, 0xd5, 0x89, 0xba, 0xab, 0xb2, 0x29, 0xa3, 0x40, 0xec, - 0xc3, 0x45, 0x65, 0x71, 0x8f, 0xd8, 0xcf, 0x72, 0x92, 0x9a, 0xc6, 0xf4, 0x5b, 0x1e, 0xb9, 0x03, - 0x4b, 0xba, 0xef, 0x96, 0x6c, 0xd6, 0xf7, 0x0f, 0xf7, 0xb7, 0x2a, 0x70, 0xa5, 0x17, 0xee, 0x02, - 0x14, 0x2d, 0xa2, 0xa4, 0x77, 0x5e, 0x27, 0xab, 0x61, 0x62, 0x4d, 0x3f, 0xe9, 0x58, 0x74, 0xc8, - 0xba, 0x1d, 0xa8, 0xe4, 0x2b, 0x05, 0x7e, 0x6d, 0x6f, 0xea, 0x2b, 0x08, 0xd2, 0x4d, 0xc1, 0xbb, - 0x55, 0xb2, 0x8c, 0xbc, 0x8b, 0xd9, 0x99, 0xee, 0x97, 0x7a, 0x00, 0x6d, 0xab, 0xed, 0x94, 0x68, - 0x0a, 0xd5, 0x96, 0xd5, 0x7e, 0xbf, 0x6e, 0x4a, 0x6d, 0xf7, 0x37, 0xa0, 0xeb, 0xf4, 0x8f, 0x9a, - 0x97, 0x51, 0xd7, 0x9d, 0x6a, 0x5e, 0x46, 0x7d, 0xcb, 0xe9, 0xb7, 0xa1, 0x6d, 0x75, 0x7b, 0x12, - 0xab, 0x42, 0x5d, 0xea, 0xe6, 0x34, 0x3b, 0xaa, 0x69, 0x0e, 0xa5, 0x1b, 0xe2, 0xbc, 0xcb, 0xb4, - 0x85, 0xe7, 0x15, 0x4d, 0x43, 0x28, 0x24, 0xdf, 0x85, 0x65, 0xb7, 0xcb, 0xd3, 0xbc, 0xaa, 0xda, - 0x7e, 0x51, 0xf3, 0xaa, 0xce, 0x69, 0x0d, 0x55, 0x02, 0x79, 0x63, 0xdd, 0x2c, 0xb2, 0xf3, 0x42, - 0xa5, 0x38, 0x5f, 0x92, 0x6f, 0xa1, 0xea, 0x50, 0x5d, 0x5c, 0xa4, 0xe8, 0x7a, 0x75, 0x7b, 0xbd, - 0x8c, 0xb4, 0x57, 0x1a, 0xbe, 0xe8, 0x9a, 0x20, 0xde, 0x26, 0xc5, 0x09, 0xc8, 0x27, 0xb0, 0xa8, - 0xba, 0xb9, 0xc8, 0xc5, 0x42, 0xaa, 0xad, 0x84, 0x50, 0x7f, 0xb3, 0x0c, 0x56, 0xc4, 0xd6, 0x05, - 0xb1, 0x2e, 0x69, 0x23, 0xb1, 0x31, 0xe3, 0x21, 0xd2, 0x88, 0x61, 0xa5, 0x54, 0x95, 0x32, 0x8f, - 0xa5, 0xbe, 0xa6, 0xdd, 0xbf, 0xfa, 0xea, 0x62, 0x96, 0xab, 0x66, 0xb4, 0x7a, 0xd9, 0xd1, 0x2d, - 0x08, 0xbf, 0x0d, 0x1d, 0xbb, 0x79, 0xd0, 0xe8, 0xec, 0x9a, 0x46, 0x43, 0xa3, 0xb3, 0xeb, 0xba, - 0x0d, 0xf5, 0xe5, 0x92, 0x8e, 0xbd, 0x0c, 0xf9, 0x36, 0xac, 0x58, 0xf5, 0xcf, 0xc3, 0x59, 0x3c, - 0x34, 0xc2, 0x53, 0xed, 0x43, 0xe9, 0xd7, 0x19, 0x4f, 0xba, 0x25, 0x08, 0xaf, 0x51, 0x87, 0x30, - 0x0a, 0xce, 0x7d, 0x68, 0xdb, 0xb5, 0xd5, 0x57, 0xd0, 0xdd, 0xb2, 0xa6, 0xec, 0xe6, 0x8d, 0x5b, - 0x1e, 0xf9, 0x0b, 0x0f, 0x3a, 0x76, 0x87, 0x13, 0x71, 0xb2, 0x2a, 0x25, 0x3a, 0x3d, 0x7b, 0xce, - 0x26, 0x44, 0x9f, 0x88, 0x4d, 0xee, 0xdd, 0x78, 0xe4, 0x30, 0xf9, 0x85, 0xe3, 0x14, 0xdd, 0xb4, - 0xff, 0x93, 0xf1, 0xb2, 0x3c, 0x69, 0xf7, 0xe9, 0xbc, 0xbc, 0xe5, 0x91, 0x0f, 0xe5, 0x3f, 0x6f, - 0x74, 0x80, 0x42, 0x2c, 0xc5, 0x56, 0x66, 0x97, 0xfd, 0x77, 0x96, 0x6d, 0xef, 0x96, 0x47, 0x7e, - 0x47, 0xfe, 0x0d, 0x43, 0x7d, 0x2b, 0xb8, 0xfe, 0xa6, 0xdf, 0xd3, 0xb7, 0xc5, 0x49, 0xae, 0xd2, - 0x4b, 0xce, 0x49, 0xca, 0x9a, 0xfd, 0x00, 0xa0, 0x88, 0x36, 0x49, 0x29, 0xf4, 0x32, 0x3a, 0xaf, - 0x1a, 0x90, 0xba, 0xb7, 0xa9, 0x23, 0x34, 0xa9, 0x06, 0x3a, 0x56, 0x9c, 0x97, 0x9b, 0xeb, 0xac, - 0x46, 0x8d, 0xfd, 0x7e, 0xdd, 0x94, 0xa2, 0xff, 0x55, 0x41, 0xff, 0x2d, 0x72, 0xd9, 0xa6, 0xbf, - 0xf3, 0xc2, 0x8e, 0x32, 0x5f, 0x92, 0xcf, 0xa1, 0xbb, 0x9f, 0x24, 0xcf, 0xa6, 0xa9, 0x49, 0x68, - 0xb8, 0x71, 0x13, 0x46, 0xba, 0xfd, 0xd2, 0xa1, 0xe8, 0x75, 0x41, 0xf9, 0x32, 0xb9, 0xe4, 0x52, - 0x2e, 0x62, 0xdf, 0x97, 0x24, 0x80, 0x35, 0x63, 0xef, 0xcc, 0x41, 0xfa, 0x2e, 0x1d, 0x3b, 0x04, - 0xad, 0xac, 0xe1, 0x78, 0x20, 0x66, 0x8d, 0x5c, 0xd3, 0xbc, 0xe5, 0x91, 0x03, 0xe8, 0x3c, 0x60, - 0xc3, 0x64, 0xc4, 0x54, 0xa8, 0xb3, 0x5e, 0xec, 0xdc, 0xc4, 0x48, 0xfd, 0xae, 0x03, 0x74, 0x35, - 0x40, 0x1a, 0xcc, 0x32, 0xf6, 0xe5, 0xce, 0x0b, 0x15, 0x44, 0xbd, 0xd4, 0x1a, 0x40, 0x07, 0x7e, - 0x8e, 0x06, 0x28, 0x45, 0x8a, 0x8e, 0x06, 0xa8, 0x44, 0x8a, 0x8e, 0x06, 0xd0, 0x81, 0x27, 0x89, - 0x30, 0x7e, 0x2c, 0x05, 0x97, 0xc6, 0x6a, 0x9e, 0x17, 0x92, 0xf6, 0xaf, 0x9d, 0x8f, 0xe0, 0xae, - 0x76, 0xc3, 0x5d, 0xed, 0x10, 0xba, 0x0f, 0x98, 0x64, 0x96, 0xac, 0x12, 0xf4, 0x5d, 0x95, 0x62, - 0x57, 0x14, 0xca, 0xea, 0x46, 0xcc, 0xb9, 0x2a, 0x5e, 0xa4, 0xe8, 0xc9, 0x77, 0xa0, 0xfd, 0x98, - 0x71, 0x5d, 0x16, 0x30, 0xbe, 0x47, 0xa9, 0x4e, 0xd0, 0xaf, 0xa9, 0x2a, 0xd0, 0x6b, 0x82, 0x5a, - 0x9f, 0xf4, 0x0c, 0xb5, 0x1d, 0x36, 0x1a, 0x33, 0xf9, 0xf8, 0x07, 0xe1, 0xe8, 0x25, 0xf9, 0x4d, - 0x41, 0xdc, 0x54, 0x12, 0x37, 0xad, 0x6c, 0xb2, 0x4d, 0x7c, 0xa5, 0x04, 0xaf, 0xa3, 0x1c, 0x27, - 0x23, 0x66, 0x19, 0xbb, 0x18, 0xda, 0x56, 0xd9, 0xd8, 0x3c, 0xa8, 0x6a, 0x2d, 0xda, 0x3c, 0xa8, - 0x9a, 0x2a, 0x33, 0xdd, 0x16, 0xeb, 0x50, 0x72, 0xad, 0x58, 0x47, 0x56, 0x96, 0x8b, 0x95, 0x76, - 0x5e, 0x04, 0x13, 0xfe, 0x92, 0x7c, 0x21, 0x5a, 0x9d, 0xed, 0xd2, 0x47, 0xe1, 0xfb, 0x94, 0xab, - 0x24, 0x86, 0x59, 0xd6, 0x94, 0xeb, 0x0f, 0xc9, 0xa5, 0x84, 0x4d, 0xfc, 0x3a, 0xc0, 0x21, 0x4f, - 0xd2, 0x07, 0x01, 0x9b, 0x24, 0x71, 0xa1, 0xc9, 0x8a, 0xf4, 0x7e, 0xa1, 0xc9, 0xac, 0x1c, 0x3f, - 0xf9, 0xc2, 0xf2, 0x3e, 0x9d, 0xca, 0x91, 0x16, 0xae, 0x73, 0x2b, 0x00, 0x86, 0x21, 0x35, 0x55, - 0x80, 0x5b, 0x1e, 0xfa, 0x92, 0x45, 0x2a, 0xc3, 0xf8, 0x92, 0x95, 0x2c, 0x89, 0x51, 0x83, 0x35, - 0x79, 0x8f, 0x03, 0x68, 0x15, 0xf1, 0xb4, 0x36, 0x4f, 0xe5, 0xe8, 0xdb, 0xd8, 0x9b, 0x4a, 0x98, - 0x4b, 0x57, 0x05, 0xab, 0x80, 0x2c, 0x21, 0xab, 0x44, 0xe5, 0xfb, 0x29, 0x80, 0xdc, 0xe0, 0x23, - 0x1c, 0x59, 0x24, 0x9d, 0x68, 0xd6, 0x26, 0xe9, 0x86, 0x8d, 0xda, 0x1d, 0xa1, 0x86, 0xe4, 0x87, - 0xde, 0x8d, 0xa3, 0x05, 0xf1, 0x0f, 0xe1, 0xaf, 0xfd, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0x6e, - 0x8c, 0x0d, 0xd9, 0x53, 0x3c, 0x00, 0x00, + 0x02, 0xed, 0x61, 0xb9, 0x70, 0x81, 0x03, 0x7f, 0x01, 0x12, 0x7f, 0xc0, 0x4a, 0x08, 0xa4, 0x3d, + 0x21, 0xb8, 0xc1, 0x09, 0xce, 0x1c, 0xb8, 0xa1, 0x17, 0x5f, 0x19, 0x91, 0x99, 0x6d, 0x7b, 0xd9, + 0x85, 0x5b, 0xc5, 0x8b, 0x97, 0x2f, 0x22, 0x5e, 0xbc, 0x78, 0xdf, 0x05, 0xad, 0x2c, 0x1d, 0xde, + 0x4c, 0xb3, 0x84, 0x27, 0x64, 0x3e, 0x8a, 0xb3, 0x74, 0xd8, 0xbf, 0x32, 0x4e, 0x92, 0x71, 0xc4, + 0x76, 0x82, 0x34, 0xdc, 0x09, 0xe2, 0x38, 0xe1, 0x01, 0x0f, 0x93, 0x38, 0x97, 0x48, 0xf4, 0x36, + 0xac, 0xdf, 0xcf, 0x58, 0xc0, 0xd9, 0x17, 0x41, 0x14, 0x31, 0xee, 0xb3, 0x2f, 0xa7, 0x2c, 0xe7, + 0xa4, 0x0f, 0x4b, 0x69, 0x90, 0xe7, 0x67, 0x49, 0x36, 0xea, 0x79, 0xd7, 0xbc, 0xed, 0x8e, 0x6f, + 0xc6, 0x74, 0x13, 0x36, 0xdc, 0x4f, 0xf2, 0x34, 0x89, 0x73, 0x86, 0xa4, 0x3e, 0x8b, 0xa3, 0x64, + 0xf8, 0xec, 0xa7, 0x22, 0xe5, 0x7e, 0xa2, 0x48, 0xfd, 0xb0, 0x01, 0xed, 0xa7, 0x59, 0x10, 0xe7, + 0xc1, 0x10, 0x37, 0x4b, 0x7a, 0xb0, 0xc8, 0x9f, 0x0f, 0x4e, 0x82, 0xfc, 0x44, 0x90, 0x68, 0xf9, + 0x7a, 0x48, 0x36, 0x61, 0x21, 0x98, 0x24, 0xd3, 0x98, 0xf7, 0x1a, 0xd7, 0xbc, 0xed, 0x39, 0x5f, + 0x8d, 0xc8, 0x7b, 0xb0, 0x16, 0x4f, 0x27, 0x83, 0x61, 0x12, 0x1f, 0x87, 0xd9, 0x44, 0x1e, 0xb9, + 0x37, 0x77, 0xcd, 0xdb, 0x9e, 0xf7, 0xab, 0x13, 0xe4, 0x2a, 0xc0, 0x11, 0x6e, 0x43, 0x2e, 0xd1, + 0x14, 0x4b, 0x58, 0x10, 0x42, 0xa1, 0xa3, 0x46, 0x2c, 0x1c, 0x9f, 0xf0, 0xde, 0xbc, 0x20, 0xe4, + 0xc0, 0x90, 0x06, 0x0f, 0x27, 0x6c, 0x90, 0xf3, 0x60, 0x92, 0xf6, 0x16, 0xc4, 0x6e, 0x2c, 0x88, + 0x98, 0x4f, 0x78, 0x10, 0x0d, 0x8e, 0x19, 0xcb, 0x7b, 0x8b, 0x6a, 0xde, 0x40, 0xc8, 0x3b, 0xb0, + 0x3c, 0x62, 0x39, 0x1f, 0x04, 0xa3, 0x51, 0xc6, 0xf2, 0x9c, 0xe5, 0xbd, 0xa5, 0x6b, 0x73, 0xdb, + 0x2d, 0xbf, 0x04, 0xa5, 0x3d, 0xd8, 0x7c, 0xcc, 0xb8, 0xc5, 0x9d, 0x5c, 0x71, 0x9a, 0xee, 0x03, + 0xb1, 0xc0, 0x0f, 0x18, 0x0f, 0xc2, 0x28, 0x27, 0xef, 0x43, 0x87, 0x5b, 0xc8, 0x3d, 0xef, 0xda, + 0xdc, 0x76, 0x7b, 0x97, 0xdc, 0x14, 0xd2, 0x71, 0xd3, 0xfa, 0xc0, 0x77, 0xf0, 0xe8, 0x3f, 0x7a, + 0xd0, 0x3e, 0x64, 0xf1, 0x48, 0xdf, 0x23, 0x81, 0x26, 0xee, 0x44, 0xdd, 0xa1, 0xf8, 0x4d, 0xbe, + 0x02, 0x6d, 0xb1, 0xbb, 0x9c, 0x67, 0x61, 0x3c, 0x16, 0x57, 0xd0, 0xf2, 0x01, 0x41, 0x87, 0x02, + 0x42, 0x56, 0x61, 0x2e, 0x98, 0x70, 0xc1, 0xf8, 0x39, 0x1f, 0x7f, 0x92, 0xeb, 0xd0, 0x49, 0x83, + 0xd9, 0x84, 0xc5, 0xbc, 0x60, 0x76, 0xc7, 0x6f, 0x2b, 0xd8, 0x1e, 0x72, 0xfb, 0x26, 0xac, 0xdb, + 0x28, 0x9a, 0xfa, 0xbc, 0xa0, 0xbe, 0x66, 0x61, 0xaa, 0x45, 0xde, 0x85, 0x15, 0x8d, 0x9f, 0xc9, + 0xcd, 0x0a, 0xf6, 0xb7, 0xfc, 0x65, 0x05, 0xd6, 0x0c, 0xfa, 0x53, 0x0f, 0x3a, 0xf2, 0x48, 0x52, + 0xce, 0xc8, 0xdb, 0xd0, 0xd5, 0x5f, 0xb2, 0x2c, 0x4b, 0x32, 0x25, 0x5d, 0x2e, 0x90, 0xdc, 0x80, + 0x55, 0x0d, 0x48, 0x33, 0x16, 0x4e, 0x82, 0x31, 0x13, 0x47, 0xed, 0xf8, 0x15, 0x38, 0xd9, 0x2d, + 0x28, 0x66, 0xc9, 0x94, 0x33, 0x71, 0xf4, 0xf6, 0x6e, 0x47, 0xb1, 0xdb, 0x47, 0x98, 0xef, 0xa2, + 0xd0, 0xef, 0x7b, 0xd0, 0xb9, 0x7f, 0x12, 0xc4, 0x31, 0x8b, 0x0e, 0x92, 0x30, 0xe6, 0x28, 0x6e, + 0xc7, 0xd3, 0x78, 0x14, 0xc6, 0xe3, 0x01, 0x7f, 0x1e, 0xea, 0x67, 0xe3, 0xc0, 0x70, 0x53, 0xf6, + 0x18, 0x99, 0xa4, 0xf8, 0x5f, 0x81, 0x23, 0xbd, 0x64, 0xca, 0xd3, 0x29, 0x1f, 0x84, 0xf1, 0x88, + 0x3d, 0x17, 0x7b, 0xea, 0xfa, 0x0e, 0x8c, 0xfe, 0x2a, 0xac, 0xee, 0xa3, 0x1c, 0xc7, 0x61, 0x3c, + 0xbe, 0x2b, 0x85, 0x0d, 0x1f, 0x57, 0x3a, 0x3d, 0x7a, 0xc6, 0x66, 0x8a, 0x2f, 0x6a, 0x84, 0xa2, + 0x70, 0x92, 0xe4, 0x5c, 0xad, 0x27, 0x7e, 0xd3, 0x7f, 0xf5, 0x60, 0x05, 0x79, 0xfb, 0x49, 0x10, + 0xcf, 0xb4, 0xc8, 0xec, 0x43, 0x07, 0x49, 0x3d, 0x4d, 0xee, 0xca, 0x27, 0x2a, 0x45, 0x6f, 0x5b, + 0xf1, 0xa2, 0x84, 0x7d, 0xd3, 0x46, 0x7d, 0x18, 0xf3, 0x6c, 0xe6, 0x3b, 0x5f, 0xa3, 0xb0, 0xf1, + 0x20, 0x1b, 0x33, 0x2e, 0x1e, 0xaf, 0x7a, 0xcc, 0x20, 0x41, 0xf7, 0x93, 0xf8, 0x98, 0x5c, 0x83, + 0x4e, 0x1e, 0xf0, 0x41, 0xca, 0xb2, 0xc1, 0xd1, 0x8c, 0x33, 0x21, 0x30, 0x73, 0x3e, 0xe4, 0x01, + 0x3f, 0x60, 0xd9, 0xbd, 0x19, 0x67, 0xfd, 0x8f, 0x60, 0xad, 0xb2, 0x0a, 0xca, 0x68, 0x71, 0x44, + 0xfc, 0x49, 0x36, 0x60, 0xfe, 0x34, 0x88, 0xa6, 0x4c, 0xe9, 0x14, 0x39, 0xf8, 0xb0, 0xf1, 0x81, + 0x47, 0xdf, 0x81, 0xd5, 0x62, 0xdb, 0x4a, 0x88, 0x08, 0x34, 0xcd, 0x2d, 0xb5, 0x7c, 0xf1, 0x9b, + 0xfe, 0xae, 0x27, 0x11, 0xef, 0x27, 0xa1, 0x79, 0x9f, 0x88, 0x88, 0xcf, 0x58, 0x23, 0xe2, 0xef, + 0x73, 0xf5, 0xd7, 0xcf, 0x7e, 0x58, 0xfa, 0x2e, 0xac, 0x59, 0x5b, 0x78, 0xc5, 0x66, 0xff, 0xc2, + 0x83, 0xb5, 0x27, 0xec, 0x4c, 0xdd, 0xba, 0xde, 0xed, 0x07, 0xd0, 0xe4, 0xb3, 0x94, 0x09, 0xcc, + 0xe5, 0xdd, 0xb7, 0xd5, 0xa5, 0x55, 0xf0, 0x6e, 0xaa, 0xe1, 0xd3, 0x59, 0xca, 0x7c, 0xf1, 0x05, + 0xfd, 0x14, 0xda, 0x16, 0x90, 0x6c, 0xc1, 0xfa, 0x17, 0x1f, 0x3f, 0x7d, 0xf2, 0xf0, 0xf0, 0x70, + 0x70, 0xf0, 0xd9, 0xbd, 0x6f, 0x3e, 0xfc, 0x8d, 0xc1, 0xde, 0xdd, 0xc3, 0xbd, 0xd5, 0x0b, 0x64, + 0x13, 0xc8, 0x93, 0x87, 0x87, 0x4f, 0x1f, 0x3e, 0x70, 0xe0, 0x1e, 0x59, 0x81, 0xb6, 0x0d, 0x68, + 0xd0, 0x3e, 0xf4, 0x9e, 0xb0, 0xb3, 0x2f, 0x42, 0x1e, 0xb3, 0x3c, 0x77, 0x97, 0xa7, 0x37, 0x81, + 0xd8, 0x7b, 0x52, 0xc7, 0xec, 0xc1, 0xa2, 0xd2, 0x98, 0xda, 0x60, 0xa8, 0x21, 0x7d, 0x07, 0xc8, + 0x61, 0x38, 0x8e, 0x3f, 0x61, 0x79, 0x1e, 0x8c, 0x99, 0x3e, 0xec, 0x2a, 0xcc, 0x4d, 0xf2, 0xb1, + 0x7a, 0x68, 0xf8, 0x93, 0x7e, 0x0d, 0xd6, 0x1d, 0x3c, 0x45, 0xf8, 0x0a, 0xb4, 0xf2, 0x70, 0x1c, + 0x07, 0x7c, 0x9a, 0x31, 0x45, 0xba, 0x00, 0xd0, 0x47, 0xb0, 0xf1, 0x39, 0xcb, 0xc2, 0xe3, 0xd9, + 0xeb, 0xc8, 0xbb, 0x74, 0x1a, 0x65, 0x3a, 0x0f, 0xe1, 0x62, 0x89, 0x8e, 0x5a, 0x5e, 0x4a, 0xa6, + 0xba, 0xbf, 0x25, 0x5f, 0x0e, 0xac, 0x77, 0xda, 0xb0, 0xdf, 0x29, 0xfd, 0x0c, 0xc8, 0xfd, 0x24, + 0x8e, 0xd9, 0x90, 0x1f, 0x30, 0x96, 0xe9, 0xcd, 0xfc, 0x7f, 0x4b, 0x0c, 0xdb, 0xbb, 0x5b, 0xea, + 0x62, 0xcb, 0x8f, 0x5f, 0xc9, 0x27, 0x81, 0x66, 0xca, 0xb2, 0x89, 0x20, 0xbc, 0xe4, 0x8b, 0xdf, + 0x74, 0x07, 0xd6, 0x1d, 0xb2, 0x05, 0xcf, 0x53, 0xc6, 0xb2, 0x81, 0xda, 0xdd, 0xbc, 0xaf, 0x87, + 0xf4, 0x36, 0x5c, 0x7c, 0x10, 0xe6, 0xc3, 0xea, 0x56, 0xf0, 0x93, 0xe9, 0xd1, 0xa0, 0x78, 0x7e, + 0x7a, 0x88, 0x56, 0xae, 0xfc, 0x89, 0xf2, 0x0d, 0xfe, 0xc0, 0x83, 0xe6, 0xde, 0xd3, 0xfd, 0xfb, + 0xe8, 0x58, 0x84, 0xf1, 0x30, 0x99, 0xa0, 0x6d, 0x90, 0xec, 0x30, 0xe3, 0x73, 0x9f, 0xd5, 0x15, + 0x68, 0x09, 0x93, 0x82, 0x86, 0x5b, 0x3c, 0xaa, 0x8e, 0x5f, 0x00, 0xd0, 0x69, 0x60, 0xcf, 0xd3, + 0x30, 0x13, 0x5e, 0x81, 0xb6, 0xf5, 0x4d, 0xa1, 0x2c, 0xab, 0x13, 0xf4, 0xdf, 0x9b, 0xd0, 0xbd, + 0x3b, 0xe4, 0xe1, 0x29, 0x53, 0xca, 0x5b, 0xac, 0x2a, 0x00, 0x6a, 0x3f, 0x6a, 0x84, 0x66, 0x26, + 0x63, 0x93, 0x84, 0xb3, 0x81, 0x73, 0x4d, 0x2e, 0x10, 0xb1, 0x86, 0x92, 0xd0, 0x20, 0x45, 0x33, + 0x20, 0xf6, 0xd7, 0xf2, 0x5d, 0x20, 0xb2, 0x0c, 0x01, 0xc8, 0x65, 0xdc, 0x59, 0xd3, 0xd7, 0x43, + 0xe4, 0xc7, 0x30, 0x48, 0x83, 0x61, 0xc8, 0x67, 0x4a, 0x1b, 0x98, 0x31, 0xd2, 0x8e, 0x92, 0x61, + 0x10, 0x0d, 0x8e, 0x82, 0x28, 0x88, 0x87, 0x4c, 0xf9, 0x27, 0x2e, 0x10, 0x5d, 0x10, 0xb5, 0x25, + 0x8d, 0x26, 0xdd, 0x94, 0x12, 0x14, 0x5d, 0x99, 0x61, 0x32, 0x99, 0x84, 0x1c, 0x3d, 0x97, 0xde, + 0x92, 0xd4, 0x3c, 0x05, 0x44, 0x9c, 0x44, 0x8e, 0xce, 0x24, 0x0f, 0x5b, 0x72, 0x35, 0x07, 0x88, + 0x54, 0x8e, 0x19, 0x13, 0x1a, 0xec, 0xd9, 0x59, 0x0f, 0x24, 0x95, 0x02, 0x82, 0xb7, 0x31, 0x8d, + 0x73, 0xc6, 0x79, 0xc4, 0x46, 0x66, 0x43, 0x6d, 0x81, 0x56, 0x9d, 0x20, 0xb7, 0x60, 0x5d, 0x3a, + 0x53, 0x79, 0xc0, 0x93, 0xfc, 0x24, 0xcc, 0x07, 0x39, 0x8b, 0x79, 0xaf, 0x23, 0xf0, 0xeb, 0xa6, + 0xc8, 0x07, 0xb0, 0x55, 0x02, 0x67, 0x6c, 0xc8, 0xc2, 0x53, 0x36, 0xea, 0x75, 0xc5, 0x57, 0xe7, + 0x4d, 0x93, 0x6b, 0xd0, 0x46, 0x1f, 0x72, 0x9a, 0x8e, 0x02, 0xce, 0xf2, 0xde, 0xb2, 0xb8, 0x07, + 0x1b, 0x44, 0x6e, 0x43, 0x37, 0x65, 0xd2, 0x0a, 0x9f, 0xf0, 0x68, 0x98, 0xf7, 0x56, 0x84, 0xe9, + 0x6b, 0xab, 0xc7, 0x86, 0xf2, 0xeb, 0xbb, 0x18, 0x28, 0x9a, 0xc3, 0xfc, 0x74, 0x30, 0x62, 0x51, + 0x30, 0xeb, 0xad, 0x0a, 0xa1, 0x2b, 0x00, 0xf4, 0x22, 0xac, 0xef, 0x87, 0x39, 0x57, 0x92, 0x66, + 0xb4, 0xdf, 0x1e, 0x6c, 0xb8, 0x60, 0xf5, 0x16, 0x6f, 0xc1, 0x92, 0x12, 0x9b, 0xbc, 0xd7, 0x16, + 0x4b, 0x6f, 0xa8, 0xa5, 0x1d, 0x89, 0xf5, 0x0d, 0x16, 0xfd, 0xfd, 0x06, 0x34, 0xf1, 0x9d, 0x9d, + 0xff, 0x26, 0xed, 0x07, 0xde, 0x70, 0x1e, 0xb8, 0xad, 0x6e, 0xe7, 0x1c, 0x75, 0x2b, 0x3c, 0xeb, + 0x19, 0x67, 0xea, 0x36, 0xa4, 0xc4, 0x5a, 0x90, 0x62, 0x3e, 0x63, 0xc3, 0x53, 0x21, 0xb6, 0x66, + 0x1e, 0x21, 0x28, 0xd4, 0x68, 0xe6, 0xc4, 0xd7, 0x52, 0x66, 0xcd, 0x58, 0xcf, 0x89, 0x2f, 0x17, + 0x8b, 0x39, 0xf1, 0x5d, 0x0f, 0x16, 0xc3, 0xf8, 0x28, 0x99, 0xc6, 0x23, 0x21, 0x9f, 0x4b, 0xbe, + 0x1e, 0x22, 0x9f, 0x53, 0xe1, 0x1d, 0x85, 0x13, 0xa6, 0x04, 0xb3, 0x00, 0x50, 0x82, 0x6e, 0x50, + 0x2e, 0x34, 0x8e, 0x61, 0xf2, 0xfb, 0xb0, 0x66, 0xc1, 0x14, 0x87, 0xaf, 0xc3, 0x3c, 0x9e, 0x5e, + 0xfb, 0xd3, 0xfa, 0x66, 0x85, 0xaa, 0x92, 0x33, 0x74, 0x15, 0x96, 0x1f, 0x33, 0xfe, 0x71, 0x7c, + 0x9c, 0x68, 0x4a, 0x7f, 0x38, 0x07, 0x2b, 0x06, 0xa4, 0x08, 0x6d, 0xc3, 0x4a, 0x38, 0x62, 0x31, + 0x0f, 0xf9, 0x6c, 0xe0, 0x78, 0x5b, 0x65, 0x30, 0x2a, 0xff, 0x20, 0x0a, 0x83, 0x5c, 0xa9, 0x0f, + 0x39, 0x20, 0xbb, 0xb0, 0x81, 0x92, 0xa7, 0x85, 0xc9, 0x5c, 0xbb, 0x74, 0xf2, 0x6a, 0xe7, 0xf0, + 0xb1, 0x20, 0x5c, 0xaa, 0xa7, 0xe2, 0x13, 0xa9, 0xea, 0xea, 0xa6, 0x90, 0x6b, 0x92, 0x12, 0x1e, + 0x79, 0x5e, 0x4a, 0xa7, 0x01, 0x54, 0xe2, 0xa3, 0x05, 0xe9, 0x60, 0x96, 0xe3, 0x23, 0x2b, 0xc6, + 0x5a, 0xaa, 0xc4, 0x58, 0xdb, 0xb0, 0x92, 0xcf, 0xe2, 0x21, 0x1b, 0x0d, 0x78, 0x82, 0xeb, 0x86, + 0xb1, 0xb8, 0x9d, 0x25, 0xbf, 0x0c, 0x16, 0xd1, 0x20, 0xcb, 0x79, 0xcc, 0xb8, 0xd0, 0x1a, 0x4b, + 0xbe, 0x1e, 0xa2, 0x02, 0x16, 0x28, 0x52, 0xe8, 0x5b, 0xbe, 0x1a, 0xa1, 0x15, 0x9b, 0x66, 0x61, + 0xde, 0xeb, 0x08, 0xa8, 0xf8, 0x4d, 0xbf, 0x27, 0x8c, 0xa3, 0x09, 0x02, 0x3f, 0x13, 0x2f, 0x97, + 0x5c, 0x86, 0x96, 0xdc, 0x53, 0x7e, 0x12, 0xe8, 0x70, 0x55, 0x00, 0x0e, 0x4f, 0x02, 0x8c, 0x5d, + 0x9c, 0x63, 0xca, 0x57, 0xd0, 0x16, 0xb0, 0x3d, 0x79, 0xca, 0xb7, 0x61, 0x59, 0x87, 0x97, 0xf9, + 0x20, 0x62, 0xc7, 0x5c, 0x3b, 0xdb, 0xf1, 0x74, 0x82, 0xcb, 0xe5, 0xfb, 0xec, 0x98, 0xd3, 0x27, + 0xb0, 0xa6, 0x5e, 0xe0, 0xa7, 0x29, 0xd3, 0x4b, 0x7f, 0xa3, 0xac, 0xff, 0xa5, 0x81, 0x5e, 0x57, + 0x92, 0x65, 0x47, 0x08, 0x25, 0xa3, 0x40, 0x7d, 0x20, 0x6a, 0xfa, 0x7e, 0x94, 0xe4, 0x4c, 0x11, + 0xa4, 0xd0, 0x19, 0x46, 0x49, 0x5e, 0x0e, 0x23, 0x6c, 0x18, 0xf2, 0x32, 0x9f, 0x0e, 0x87, 0xf8, + 0x72, 0xa5, 0x89, 0xd7, 0x43, 0xfa, 0x57, 0x1e, 0xac, 0x0b, 0x6a, 0x5a, 0x57, 0x18, 0xbf, 0xf0, + 0xcd, 0xb7, 0xd9, 0x19, 0xda, 0x61, 0xcd, 0x06, 0xcc, 0x1f, 0x27, 0xd9, 0x90, 0xa9, 0x95, 0xe4, + 0xe0, 0xe7, 0xe1, 0xe9, 0xfe, 0x93, 0x07, 0x6b, 0x62, 0xab, 0x87, 0x3c, 0xe0, 0xd3, 0x5c, 0x1d, + 0xff, 0x97, 0xa1, 0x8b, 0x47, 0x65, 0x5a, 0xfc, 0xd5, 0x46, 0x37, 0xcc, 0x4b, 0x15, 0x50, 0x89, + 0xbc, 0x77, 0xc1, 0x77, 0x91, 0xc9, 0x47, 0xd0, 0xb1, 0x73, 0x04, 0x62, 0xcf, 0xed, 0xdd, 0x4b, + 0xfa, 0x94, 0x15, 0xc9, 0xd9, 0xbb, 0xe0, 0x3b, 0x1f, 0x90, 0x3b, 0x00, 0xc2, 0x32, 0x0b, 0xb2, + 0x2a, 0x0c, 0xbc, 0xe4, 0x32, 0xc9, 0xba, 0xac, 0xbd, 0x0b, 0xbe, 0x85, 0x7e, 0x6f, 0x09, 0x16, + 0xa4, 0x29, 0xa1, 0x8f, 0xa1, 0xeb, 0xec, 0xd4, 0xf1, 0xe0, 0x3b, 0xd2, 0x83, 0xaf, 0x04, 0x78, + 0x8d, 0x9a, 0x00, 0xef, 0x6f, 0x1b, 0x40, 0x50, 0xda, 0x4a, 0xd7, 0xf9, 0x0e, 0x2c, 0x2b, 0xf6, + 0xbb, 0xce, 0x5b, 0x09, 0x2a, 0x6c, 0x5e, 0x32, 0x72, 0x3c, 0x98, 0x8e, 0x6f, 0x83, 0xc8, 0x4d, + 0x20, 0xd6, 0x50, 0x47, 0xed, 0xd2, 0x1e, 0xd4, 0xcc, 0xa0, 0xe2, 0x92, 0xee, 0x87, 0x8e, 0x57, + 0x95, 0xc7, 0xd6, 0x14, 0xf7, 0x5b, 0x3b, 0x27, 0x92, 0x49, 0xd3, 0xfc, 0x04, 0x6d, 0xb2, 0xf6, + 0x71, 0xf4, 0xb8, 0x2c, 0x48, 0x0b, 0xaf, 0x15, 0xa4, 0xc5, 0xb2, 0x20, 0x09, 0x0b, 0x97, 0x85, + 0xa7, 0x01, 0x67, 0xda, 0x6a, 0xa8, 0x21, 0xfd, 0x89, 0x07, 0xab, 0xc8, 0x3d, 0x47, 0xc2, 0x3e, + 0x04, 0x21, 0xe0, 0x6f, 0x28, 0x60, 0x0e, 0xee, 0xcf, 0x2e, 0x5f, 0x1f, 0x40, 0x4b, 0x10, 0x4c, + 0x52, 0x16, 0x2b, 0xf1, 0xea, 0xb9, 0xe2, 0x55, 0xe8, 0x96, 0xbd, 0x0b, 0x7e, 0x81, 0x6c, 0x09, + 0xd7, 0x3f, 0x78, 0xd0, 0x56, 0xdb, 0xfc, 0x1f, 0xbb, 0xd4, 0x7d, 0x58, 0x42, 0x39, 0xb3, 0x3c, + 0x56, 0x33, 0x46, 0x9d, 0x3e, 0xc1, 0x88, 0x06, 0x8d, 0x98, 0xe3, 0x4e, 0x97, 0xc1, 0x68, 0x91, + 0x84, 0x1a, 0xcd, 0x07, 0x3c, 0x8c, 0x06, 0x7a, 0x56, 0x25, 0xda, 0xea, 0xa6, 0x50, 0x9b, 0xe4, + 0x3c, 0x18, 0x33, 0x65, 0x6c, 0xe4, 0x00, 0xe3, 0x06, 0x75, 0xa0, 0xb2, 0xab, 0xf4, 0x63, 0x80, + 0xad, 0xca, 0x94, 0x71, 0x97, 0x94, 0x87, 0x18, 0x85, 0x93, 0xa3, 0xc4, 0x38, 0x9b, 0x9e, 0xed, + 0x3c, 0x3a, 0x53, 0x64, 0x0c, 0x17, 0xb5, 0x55, 0x45, 0x9e, 0x16, 0x36, 0xb4, 0x21, 0xdc, 0x81, + 0xdb, 0xae, 0x0c, 0x94, 0x17, 0xd4, 0x70, 0xfb, 0x3d, 0xd6, 0xd3, 0x23, 0x27, 0xd0, 0x33, 0xe6, + 0x5b, 0x29, 0x6e, 0xcb, 0xc4, 0xe3, 0x5a, 0xef, 0xbd, 0x66, 0x2d, 0xa1, 0x65, 0x46, 0x7a, 0x99, + 0x73, 0xa9, 0x91, 0x19, 0x5c, 0xd5, 0x73, 0x42, 0x33, 0x57, 0xd7, 0x6b, 0xbe, 0xd1, 0xd9, 0x1e, + 0xe1, 0xc7, 0xee, 0xa2, 0xaf, 0x21, 0xdc, 0xff, 0xb1, 0x07, 0xcb, 0x2e, 0x39, 0x14, 0x1d, 0x15, + 0x75, 0x68, 0xd5, 0xa1, 0xdd, 0xa2, 0x12, 0xb8, 0x1a, 0x37, 0x35, 0xea, 0xe2, 0x26, 0x3b, 0x3a, + 0x9a, 0x7b, 0x5d, 0x74, 0xd4, 0x7c, 0xb3, 0xe8, 0x68, 0xbe, 0x2e, 0x3a, 0xea, 0xff, 0xa7, 0x07, + 0xa4, 0x7a, 0xbf, 0xe4, 0xb1, 0x0c, 0xdc, 0x62, 0x16, 0x29, 0x3d, 0xf1, 0x0b, 0x6f, 0x26, 0x23, + 0x9a, 0x87, 0xfa, 0x6b, 0x14, 0x56, 0x5b, 0x11, 0xd8, 0xce, 0x48, 0xd7, 0xaf, 0x9b, 0x2a, 0xc5, + 0x6b, 0xcd, 0xd7, 0xc7, 0x6b, 0xf3, 0xaf, 0x8f, 0xd7, 0x16, 0xca, 0xf1, 0x5a, 0xff, 0xb7, 0xa1, + 0xeb, 0xdc, 0xfa, 0xcf, 0xef, 0xc4, 0x65, 0x47, 0x46, 0x5e, 0xb0, 0x03, 0xeb, 0xff, 0x47, 0x03, + 0x48, 0x55, 0xf2, 0xfe, 0x4f, 0xf7, 0x20, 0xe4, 0xc8, 0x51, 0x20, 0x73, 0x4a, 0x8e, 0x1c, 0xd5, + 0xf1, 0xbf, 0xa9, 0x14, 0xdf, 0x83, 0xb5, 0x8c, 0x0d, 0x93, 0x53, 0x96, 0x59, 0x31, 0xb3, 0xbc, + 0xaa, 0xea, 0x04, 0xba, 0x72, 0x6e, 0x94, 0xba, 0xe4, 0xd4, 0x06, 0x2c, 0xcb, 0x50, 0x0a, 0x56, + 0xe9, 0x37, 0x60, 0x43, 0x96, 0x6c, 0xee, 0x49, 0x52, 0xda, 0x9b, 0xb8, 0x0e, 0x9d, 0x33, 0x99, + 0xa6, 0x1b, 0x24, 0x71, 0x34, 0x53, 0x46, 0xa4, 0xad, 0x60, 0x9f, 0xc6, 0xd1, 0x8c, 0xfe, 0xc8, + 0x83, 0x8b, 0xa5, 0x6f, 0x8b, 0x6c, 0xbc, 0x54, 0xb5, 0xae, 0xfe, 0x75, 0x81, 0x78, 0x44, 0x25, + 0xe3, 0xd6, 0x11, 0xa5, 0x49, 0xaa, 0x4e, 0x20, 0x0b, 0xa7, 0x71, 0x15, 0x5f, 0x5e, 0x4c, 0xdd, + 0x14, 0xdd, 0x82, 0x8b, 0xea, 0xf2, 0xdd, 0xb3, 0xd1, 0x5d, 0xd8, 0x2c, 0x4f, 0x14, 0x99, 0x2f, + 0x77, 0xcb, 0x7a, 0x48, 0x3f, 0x02, 0xf2, 0xad, 0x29, 0xcb, 0x66, 0x22, 0xef, 0x6f, 0x52, 0xab, + 0x5b, 0xe5, 0x10, 0x7b, 0x21, 0x9d, 0x1e, 0x7d, 0x93, 0xcd, 0x74, 0xb9, 0xa4, 0x61, 0xca, 0x25, + 0xf4, 0x0e, 0xac, 0x3b, 0x04, 0x0c, 0xab, 0x16, 0x44, 0xed, 0x40, 0x87, 0x9f, 0x6e, 0x7d, 0x41, + 0xcd, 0xd1, 0x3f, 0xf7, 0x60, 0x6e, 0x2f, 0x49, 0xed, 0x9c, 0x91, 0xe7, 0xe6, 0x8c, 0x94, 0xee, + 0x1c, 0x18, 0xd5, 0xd8, 0x50, 0x2f, 0xdf, 0x06, 0xa2, 0xe6, 0x0b, 0x26, 0x1c, 0x03, 0xb0, 0xe3, + 0x24, 0x3b, 0x0b, 0xb2, 0x91, 0xe2, 0x5f, 0x09, 0x8a, 0xdb, 0x2f, 0x14, 0x0c, 0xfe, 0x44, 0xa7, + 0x41, 0x24, 0xce, 0x66, 0x2a, 0x66, 0x54, 0x23, 0xfa, 0x03, 0x0f, 0xe6, 0xc5, 0x5e, 0xf1, 0x35, + 0xc8, 0xfb, 0x15, 0xa5, 0x32, 0x91, 0x97, 0xf3, 0xe4, 0x6b, 0x28, 0x81, 0x4b, 0x05, 0xb4, 0x46, + 0xa5, 0x80, 0x76, 0x05, 0x5a, 0x72, 0x54, 0x54, 0x9c, 0x0a, 0x00, 0xb9, 0x0a, 0xcd, 0x93, 0x24, + 0xd5, 0x36, 0x0c, 0x74, 0x22, 0x26, 0x49, 0x7d, 0x01, 0xa7, 0x37, 0x60, 0xe5, 0x49, 0x32, 0x62, + 0x56, 0xb4, 0x7e, 0xee, 0x35, 0xd1, 0xdf, 0xf1, 0x60, 0x49, 0x23, 0x93, 0x6d, 0x68, 0xa2, 0x29, + 0x2a, 0x39, 0x7f, 0x26, 0x9d, 0x8a, 0x78, 0xbe, 0xc0, 0x40, 0x15, 0x22, 0x62, 0xc3, 0xc2, 0x55, + 0xd0, 0x91, 0x61, 0x61, 0x84, 0xd1, 0x1d, 0x17, 0x7b, 0x2e, 0x19, 0xab, 0x12, 0x94, 0xfe, 0xb5, + 0x07, 0x5d, 0x67, 0x0d, 0x74, 0xd0, 0xa3, 0x20, 0xe7, 0x2a, 0x05, 0xa5, 0x98, 0x68, 0x83, 0xec, + 0xcc, 0x4e, 0xc3, 0xcd, 0xec, 0x98, 0xcc, 0xc2, 0x9c, 0x9d, 0x59, 0xb8, 0x05, 0xad, 0xa2, 0x18, + 0xd9, 0x74, 0x54, 0x03, 0xae, 0xa8, 0x13, 0xc5, 0x05, 0x12, 0xd2, 0x19, 0x26, 0x51, 0x92, 0xa9, + 0x5a, 0x9d, 0x1c, 0xd0, 0x3b, 0xd0, 0xb6, 0xf0, 0x71, 0x1b, 0x31, 0xe3, 0x67, 0x49, 0xf6, 0x4c, + 0x27, 0x98, 0xd4, 0xd0, 0x14, 0x48, 0x1a, 0x45, 0x81, 0x84, 0xfe, 0x8d, 0x07, 0x5d, 0x94, 0x94, + 0x30, 0x1e, 0x1f, 0x24, 0x51, 0x38, 0x9c, 0x09, 0x89, 0xd1, 0x42, 0x31, 0x18, 0xb1, 0x88, 0x07, + 0x46, 0x62, 0x5c, 0x30, 0xda, 0xfc, 0x49, 0x18, 0x0b, 0x95, 0xa5, 0xe4, 0xc5, 0x8c, 0x51, 0xf2, + 0xd1, 0x76, 0x1d, 0x05, 0x39, 0x1b, 0x4c, 0x30, 0x9c, 0x50, 0xba, 0xda, 0x01, 0xa2, 0xfa, 0x40, + 0x40, 0x16, 0x70, 0x36, 0x98, 0x84, 0x51, 0x14, 0x4a, 0x5c, 0x29, 0xe1, 0x75, 0x53, 0x18, 0x66, + 0xb5, 0x95, 0x9a, 0x78, 0x38, 0x1a, 0xcb, 0x5c, 0xa9, 0x72, 0x44, 0xcc, 0xf3, 0xb3, 0x20, 0x7a, + 0xde, 0x71, 0x5d, 0x2c, 0x48, 0xf9, 0x5a, 0xe7, 0xaa, 0xd7, 0x7a, 0x05, 0x5a, 0x28, 0x5e, 0xb7, + 0x85, 0x8f, 0x24, 0x6b, 0xd7, 0x05, 0x40, 0xcf, 0xee, 0x8a, 0xd9, 0xf9, 0x62, 0x56, 0x00, 0x1c, + 0xaf, 0x68, 0xa1, 0xe4, 0x15, 0x7d, 0x00, 0x1d, 0x45, 0x46, 0xf0, 0x5d, 0x84, 0x4b, 0x85, 0x80, + 0x3b, 0x77, 0xe2, 0x3b, 0x98, 0xfa, 0xcb, 0x5d, 0xfd, 0xe5, 0xd2, 0xeb, 0xbe, 0xd4, 0x98, 0xf4, + 0x22, 0xac, 0x2b, 0xe6, 0x3d, 0xce, 0x82, 0xf4, 0x44, 0xab, 0xde, 0x91, 0x29, 0x90, 0x0a, 0x30, + 0xb9, 0x01, 0xf3, 0xf8, 0x99, 0xd6, 0x7e, 0xf5, 0x8f, 0x4e, 0xa2, 0x90, 0x6d, 0x98, 0x67, 0xa3, + 0x31, 0xd3, 0x9e, 0x39, 0x71, 0x63, 0x24, 0xbc, 0x23, 0x5f, 0x22, 0xa0, 0x0a, 0x40, 0x68, 0x49, + 0x05, 0xb8, 0x9a, 0x73, 0x01, 0x87, 0x1f, 0x8f, 0xe8, 0x06, 0x90, 0x27, 0x52, 0x6a, 0xed, 0xfc, + 0xde, 0xef, 0xcd, 0x41, 0xdb, 0x02, 0xe3, 0x6b, 0x1e, 0xe3, 0x86, 0x07, 0xa3, 0x30, 0x98, 0x30, + 0xce, 0x32, 0x25, 0xa9, 0x25, 0xa8, 0x50, 0xb0, 0xa7, 0xe3, 0x41, 0x32, 0xe5, 0x83, 0x11, 0x1b, + 0x67, 0x4c, 0x1a, 0x34, 0xcf, 0x2f, 0x41, 0x11, 0x6f, 0x12, 0x3c, 0xb7, 0xf1, 0xa4, 0x3c, 0x94, + 0xa0, 0x3a, 0x5b, 0x27, 0x79, 0xd4, 0x2c, 0xb2, 0x75, 0x92, 0x23, 0x65, 0x3d, 0x34, 0x5f, 0xa3, + 0x87, 0xde, 0x87, 0x4d, 0xa9, 0x71, 0xd4, 0xdb, 0x1c, 0x94, 0xc4, 0xe4, 0x9c, 0x59, 0x72, 0x03, + 0x56, 0x71, 0xcf, 0x5a, 0xc0, 0xf3, 0xf0, 0x7b, 0x32, 0xce, 0xf6, 0xfc, 0x0a, 0x1c, 0x71, 0xf1, + 0x39, 0x3a, 0xb8, 0xb2, 0x98, 0x50, 0x81, 0x0b, 0xdc, 0xe0, 0xb9, 0x8b, 0xdb, 0x52, 0xb8, 0x25, + 0x38, 0xed, 0x42, 0xfb, 0x90, 0x27, 0xa9, 0xbe, 0x94, 0x65, 0xe8, 0xc8, 0xa1, 0x2a, 0x20, 0x5d, + 0x86, 0x4b, 0x42, 0x8a, 0x9e, 0x26, 0x69, 0x12, 0x25, 0xe3, 0xd9, 0xe1, 0xf4, 0x28, 0x1f, 0x66, + 0x61, 0x8a, 0x1e, 0x33, 0xfd, 0x7b, 0x0f, 0xd6, 0x9d, 0x59, 0x15, 0xea, 0xff, 0xa2, 0x14, 0x69, + 0x93, 0xf3, 0x97, 0x82, 0xb7, 0x66, 0xa9, 0x43, 0x89, 0x28, 0x53, 0x22, 0x9f, 0xa9, 0x32, 0xc0, + 0x5d, 0x58, 0xd1, 0x3b, 0xd3, 0x1f, 0x4a, 0x29, 0xec, 0x55, 0xa5, 0x50, 0x7d, 0xbf, 0xac, 0x3e, + 0xd0, 0x24, 0x7e, 0x45, 0xfa, 0x9d, 0x6c, 0x24, 0xce, 0xa8, 0x63, 0xbe, 0xbe, 0xfe, 0xde, 0x76, + 0x76, 0xf5, 0x0e, 0x86, 0x06, 0x98, 0xd3, 0x3f, 0xf2, 0x00, 0x8a, 0xdd, 0xa1, 0x60, 0x14, 0x2a, + 0xdd, 0x13, 0xd9, 0x50, 0x4b, 0x7d, 0x5f, 0x87, 0x8e, 0xc9, 0x39, 0x17, 0x56, 0xa2, 0xad, 0x61, + 0xe8, 0xa1, 0xbc, 0x0b, 0x2b, 0xe3, 0x28, 0x39, 0x12, 0x36, 0x57, 0xd4, 0x2a, 0x73, 0x55, 0x46, + 0x5b, 0x96, 0xe0, 0x47, 0x0a, 0x5a, 0x98, 0x94, 0xa6, 0x65, 0x52, 0xe8, 0x1f, 0x37, 0x4c, 0xe6, + 0xb3, 0x38, 0xf3, 0xb9, 0xaf, 0x8c, 0xec, 0x56, 0x94, 0xe3, 0x39, 0x89, 0x46, 0x91, 0xdd, 0x38, + 0x78, 0x6d, 0xa0, 0x77, 0x07, 0x96, 0x33, 0xa9, 0x7d, 0xb4, 0x6a, 0x6a, 0xbe, 0x42, 0x35, 0x75, + 0x33, 0xc7, 0xee, 0xfc, 0x3f, 0x58, 0x0d, 0x46, 0xa7, 0x2c, 0xe3, 0xa1, 0xf0, 0xf8, 0x85, 0xd1, + 0x97, 0x0a, 0x75, 0xc5, 0x82, 0x0b, 0x5b, 0xfc, 0x2e, 0xac, 0xa8, 0xd2, 0xa5, 0xc1, 0x54, 0x1d, + 0x29, 0x05, 0x18, 0x11, 0xe9, 0x5f, 0xea, 0x24, 0xab, 0x7b, 0x87, 0xe7, 0x73, 0xc4, 0x3e, 0x5d, + 0xa3, 0x74, 0xba, 0xaf, 0xaa, 0x84, 0xe7, 0x48, 0x87, 0x15, 0x2a, 0xf5, 0x2c, 0x81, 0x2a, 0x41, + 0xed, 0xb2, 0xb4, 0xf9, 0x26, 0x2c, 0xa5, 0x3f, 0x9a, 0x83, 0xc5, 0x8f, 0xe3, 0xd3, 0x24, 0x1c, + 0x8a, 0xf4, 0xe3, 0x84, 0x4d, 0x12, 0xdd, 0x40, 0x80, 0xbf, 0xd1, 0xa2, 0x8b, 0xda, 0x58, 0xca, + 0x55, 0x5e, 0x50, 0x0f, 0xd1, 0xba, 0x65, 0x45, 0xd3, 0x8c, 0x94, 0x14, 0x0b, 0x82, 0xfe, 0x61, + 0x66, 0xf7, 0x01, 0xa9, 0x51, 0xd1, 0x81, 0x31, 0x6f, 0x75, 0x60, 0x88, 0x64, 0xb5, 0x2c, 0xfb, + 0x09, 0x76, 0x2e, 0xf9, 0x7a, 0x28, 0xfc, 0xd8, 0x8c, 0xc9, 0xa0, 0x57, 0xd8, 0xc9, 0x45, 0xe5, + 0xc7, 0xda, 0x40, 0xb4, 0xa5, 0xf2, 0x03, 0x89, 0x23, 0x75, 0x8d, 0x0d, 0x42, 0xdf, 0xa2, 0xdc, + 0x4a, 0xd4, 0x92, 0x57, 0x5c, 0x02, 0xa3, 0x42, 0x1a, 0x31, 0xa3, 0x37, 0xe4, 0x19, 0x40, 0x36, + 0x05, 0x95, 0xe1, 0x96, 0x17, 0x2c, 0xcb, 0x97, 0x6a, 0x24, 0x7c, 0x90, 0x20, 0x8a, 0x8e, 0x82, + 0xe1, 0x33, 0xd1, 0xe0, 0x25, 0xaa, 0x95, 0x2d, 0xdf, 0x05, 0xe2, 0xae, 0x87, 0x11, 0x3f, 0x1d, + 0x28, 0x12, 0x5d, 0x59, 0x6d, 0xb4, 0x40, 0xf4, 0x73, 0x20, 0x77, 0x47, 0x23, 0x75, 0x43, 0x26, + 0x46, 0x28, 0x78, 0xeb, 0x39, 0xbc, 0xad, 0x39, 0x63, 0xa3, 0xf6, 0x8c, 0xf4, 0x21, 0xb4, 0x0f, + 0xac, 0xbe, 0x2c, 0x71, 0x99, 0xba, 0x23, 0x4b, 0x09, 0x80, 0x05, 0xb1, 0x16, 0x6c, 0xd8, 0x0b, + 0xd2, 0x5f, 0x02, 0xb2, 0x1f, 0xe6, 0xdc, 0xec, 0xcf, 0x84, 0x8a, 0x26, 0xe3, 0x65, 0x85, 0x8a, + 0x0a, 0x26, 0x42, 0xc5, 0xbb, 0xb2, 0xe8, 0x59, 0x3e, 0xd8, 0x0d, 0x58, 0x0a, 0x25, 0x48, 0xeb, + 0xe1, 0x65, 0x25, 0xc0, 0x1a, 0xd3, 0xcc, 0xa3, 0x43, 0xa1, 0x80, 0x8e, 0x9a, 0xff, 0x81, 0x07, + 0x8b, 0xea, 0x68, 0x68, 0x0e, 0x9d, 0x8e, 0x34, 0x79, 0x30, 0x07, 0x56, 0xdf, 0x11, 0x54, 0x95, + 0xba, 0xb9, 0x3a, 0xa9, 0x23, 0xd0, 0x4c, 0x03, 0x7e, 0x22, 0x3c, 0xe8, 0x96, 0x2f, 0x7e, 0xeb, + 0x48, 0x69, 0xde, 0x44, 0x4a, 0xba, 0xc0, 0xab, 0x36, 0x65, 0xb2, 0x96, 0xf7, 0x64, 0x81, 0xb7, + 0x00, 0x17, 0x3c, 0x50, 0x1b, 0x2c, 0xf3, 0x40, 0xa1, 0xfa, 0x66, 0x9e, 0xf6, 0xa1, 0xf7, 0x80, + 0x45, 0x8c, 0xb3, 0xbb, 0x51, 0x54, 0xa6, 0x7f, 0x19, 0x2e, 0xd5, 0xcc, 0x29, 0x4b, 0xf9, 0x08, + 0xd6, 0x1e, 0xb0, 0xa3, 0xe9, 0x78, 0x9f, 0x9d, 0x16, 0x05, 0x03, 0x02, 0xcd, 0xfc, 0x24, 0x39, + 0x53, 0xf7, 0x25, 0x7e, 0x93, 0xb7, 0x00, 0x22, 0xc4, 0x19, 0xe4, 0x29, 0x1b, 0xea, 0x76, 0x16, + 0x01, 0x39, 0x4c, 0xd9, 0x90, 0xbe, 0x0f, 0xc4, 0xa6, 0xa3, 0x8e, 0x80, 0xaf, 0x71, 0x7a, 0x34, + 0xc8, 0x67, 0x39, 0x67, 0x13, 0xdd, 0xa7, 0x63, 0x83, 0xe8, 0xbb, 0xd0, 0x39, 0x08, 0x66, 0x3e, + 0xfb, 0x52, 0x35, 0xfa, 0x61, 0x40, 0x16, 0xcc, 0x50, 0x3c, 0x4d, 0x40, 0x26, 0xa6, 0xe9, 0xdf, + 0x35, 0x60, 0x41, 0x62, 0x22, 0xd5, 0x11, 0xcb, 0x79, 0x18, 0xcb, 0xb4, 0xba, 0xa2, 0x6a, 0x81, + 0x2a, 0xf7, 0xdd, 0xa8, 0xb9, 0x6f, 0xe5, 0x22, 0xe9, 0xd2, 0xbf, 0xba, 0x58, 0x07, 0x26, 0xe2, + 0xcd, 0x70, 0xc2, 0x64, 0xbf, 0x67, 0x53, 0xc5, 0x9b, 0x1a, 0x50, 0x8a, 0x7c, 0x8b, 0x37, 0x2f, + 0xf7, 0xa7, 0x05, 0x51, 0x99, 0x05, 0x1b, 0x54, 0xab, 0x59, 0x16, 0x65, 0x67, 0x5f, 0x45, 0xb3, + 0x54, 0x34, 0xc8, 0xd2, 0x1b, 0x68, 0x10, 0xe9, 0x37, 0x39, 0x1a, 0x84, 0xc0, 0xea, 0x23, 0xc6, + 0x7c, 0x96, 0x26, 0x99, 0xe9, 0x96, 0xfc, 0xa1, 0x07, 0xab, 0xca, 0x22, 0x98, 0x39, 0x72, 0xdd, + 0x31, 0x1f, 0x5e, 0x5d, 0xa6, 0xf5, 0x6d, 0xe8, 0x8a, 0x00, 0x0a, 0xa3, 0x23, 0x11, 0x2d, 0xa9, + 0x9c, 0x82, 0x03, 0xc4, 0x3d, 0xe9, 0xdc, 0xe1, 0x24, 0x8c, 0x14, 0x83, 0x6d, 0x10, 0x9a, 0x3a, + 0x1d, 0x60, 0x09, 0xf6, 0x7a, 0xbe, 0x19, 0xd3, 0x03, 0x58, 0xb3, 0xf6, 0xab, 0x04, 0xea, 0x0e, + 0xe8, 0x7a, 0xa3, 0x4c, 0x11, 0xc8, 0x77, 0xb1, 0xe5, 0x1a, 0xb7, 0xe2, 0x33, 0x07, 0x99, 0xfe, + 0xb3, 0x07, 0xeb, 0xd2, 0xd0, 0x2b, 0x37, 0xca, 0xb4, 0x28, 0x2d, 0x48, 0xcf, 0x46, 0x0a, 0xfc, + 0xde, 0x05, 0x5f, 0x8d, 0xc9, 0xd7, 0xdf, 0xd0, 0x39, 0x31, 0xa5, 0xbd, 0x73, 0xd8, 0x33, 0x57, + 0xc7, 0x9e, 0x57, 0x1c, 0xbe, 0x2e, 0x00, 0x9e, 0xaf, 0x0d, 0x80, 0xef, 0x2d, 0xc2, 0x7c, 0x3e, + 0x4c, 0x52, 0x46, 0x37, 0x61, 0xc3, 0x3d, 0x9c, 0x64, 0xd9, 0xee, 0xbf, 0x78, 0xb0, 0x2c, 0x93, + 0x71, 0xb2, 0x0f, 0x9b, 0x65, 0x04, 0x63, 0x2d, 0xab, 0xbd, 0x9b, 0x18, 0x57, 0xb3, 0xda, 0x26, + 0xde, 0xbf, 0x5c, 0x3b, 0xa7, 0xfd, 0xec, 0xef, 0xff, 0xe4, 0xdf, 0xfe, 0xa4, 0x71, 0x91, 0xae, + 0xee, 0x9c, 0xde, 0xde, 0x11, 0x2a, 0x91, 0x9d, 0x09, 0x8c, 0x0f, 0xbd, 0x1b, 0xb8, 0x8a, 0xdd, + 0xf9, 0x6d, 0x56, 0xa9, 0xe9, 0x20, 0x37, 0xab, 0xd4, 0xb6, 0x8a, 0x3b, 0xab, 0x4c, 0x05, 0x86, + 0x59, 0x65, 0xf7, 0xbf, 0xfa, 0xd0, 0x32, 0x41, 0x21, 0xf9, 0x2e, 0x74, 0x9d, 0xc4, 0x23, 0xd1, + 0x84, 0xeb, 0x52, 0x99, 0xfd, 0x2b, 0xf5, 0x93, 0x6a, 0xd9, 0xab, 0x62, 0xd9, 0x1e, 0xd9, 0xc4, + 0x65, 0x55, 0xb6, 0x6f, 0x47, 0x64, 0x64, 0x65, 0x8f, 0xc2, 0x33, 0x58, 0x76, 0x93, 0x85, 0xe4, + 0x8a, 0x2b, 0x1a, 0xa5, 0xd5, 0xde, 0x3a, 0x67, 0x56, 0x2d, 0x77, 0x45, 0x2c, 0xb7, 0x49, 0x36, + 0xec, 0xe5, 0x4c, 0xb0, 0xc6, 0x44, 0x57, 0x89, 0xdd, 0x12, 0x4e, 0x34, 0xbd, 0xfa, 0x56, 0xf1, + 0xfe, 0xa5, 0x6a, 0xfb, 0xb7, 0xea, 0x17, 0xa7, 0x3d, 0xb1, 0x14, 0x21, 0x82, 0xa1, 0x76, 0x47, + 0x38, 0xf9, 0x0e, 0xb4, 0x4c, 0x43, 0x29, 0xd9, 0xb2, 0xba, 0x78, 0xed, 0x2e, 0xd7, 0x7e, 0xaf, + 0x3a, 0x51, 0x77, 0x55, 0x36, 0x65, 0x14, 0x88, 0x7d, 0xb8, 0xa8, 0x2c, 0xf4, 0x11, 0xfb, 0x69, + 0x4e, 0x52, 0xd3, 0xc8, 0x7e, 0xcb, 0x23, 0x77, 0x60, 0x49, 0xf7, 0xe9, 0x92, 0xcd, 0xfa, 0x7e, + 0xe3, 0xfe, 0x56, 0x05, 0xae, 0xf4, 0xc8, 0x5d, 0x80, 0xa2, 0xa5, 0x94, 0xf4, 0xce, 0xeb, 0x7c, + 0x35, 0x4c, 0xac, 0xe9, 0x3f, 0x1d, 0x8b, 0x8e, 0x5a, 0xb7, 0x63, 0x95, 0x7c, 0xa5, 0xc0, 0xaf, + 0xed, 0x65, 0x7d, 0x05, 0x41, 0xba, 0x29, 0x78, 0xb7, 0x4a, 0x96, 0x91, 0x77, 0x31, 0x3b, 0xd3, + 0xfd, 0x55, 0x0f, 0xa0, 0x6d, 0xb5, 0xa9, 0x12, 0x4d, 0xa1, 0xda, 0xe2, 0xda, 0xef, 0xd7, 0x4d, + 0xa9, 0xed, 0xfe, 0x1a, 0x74, 0x9d, 0x7e, 0x53, 0xf3, 0x32, 0xea, 0xba, 0x59, 0xcd, 0xcb, 0xa8, + 0x6f, 0x51, 0xfd, 0x36, 0xb4, 0xad, 0xee, 0x50, 0x62, 0x55, 0xb4, 0x4b, 0xdd, 0x9f, 0x66, 0x47, + 0x35, 0xcd, 0xa4, 0x74, 0x43, 0x9c, 0x77, 0x99, 0xb6, 0xf0, 0xbc, 0xa2, 0xc9, 0x08, 0x85, 0xe4, + 0xbb, 0xb0, 0xec, 0x76, 0x85, 0x9a, 0x57, 0x55, 0xdb, 0x5f, 0x6a, 0x5e, 0xd5, 0x39, 0xad, 0xa4, + 0x4a, 0x20, 0x6f, 0xac, 0x9b, 0x45, 0x76, 0x5e, 0xa8, 0x94, 0xe8, 0x4b, 0xf2, 0x2d, 0x54, 0x1d, + 0xaa, 0xeb, 0x8b, 0x14, 0x5d, 0xb2, 0x6e, 0x6f, 0x98, 0x91, 0xf6, 0x4a, 0x83, 0x18, 0x5d, 0x13, + 0xc4, 0xdb, 0xa4, 0x38, 0x01, 0xf9, 0x04, 0x16, 0x55, 0xf7, 0x17, 0xb9, 0x58, 0x48, 0xb5, 0x95, + 0x40, 0xea, 0x6f, 0x96, 0xc1, 0x8a, 0xd8, 0xba, 0x20, 0xd6, 0x25, 0x6d, 0x24, 0x36, 0x66, 0x3c, + 0x44, 0x1a, 0x31, 0xac, 0x94, 0xaa, 0x58, 0xe6, 0xb1, 0xd4, 0xd7, 0xc0, 0xfb, 0x57, 0x5f, 0x5d, + 0xfc, 0x72, 0xd5, 0x8c, 0x56, 0x2f, 0x3b, 0xba, 0x65, 0xe1, 0x37, 0xa1, 0x63, 0x37, 0x1b, 0x1a, + 0x9d, 0x5d, 0xd3, 0x98, 0x68, 0x74, 0x76, 0x5d, 0x77, 0xa2, 0xbe, 0x5c, 0xd2, 0xb1, 0x97, 0x21, + 0xdf, 0x86, 0x15, 0xab, 0x5e, 0x7a, 0x38, 0x8b, 0x87, 0x46, 0x78, 0xaa, 0x7d, 0x2b, 0xfd, 0x3a, + 0x4b, 0x4b, 0xb7, 0x04, 0xe1, 0x35, 0xea, 0x10, 0x46, 0xc1, 0xb9, 0x0f, 0x6d, 0xbb, 0x16, 0xfb, + 0x0a, 0xba, 0x5b, 0xd6, 0x94, 0xdd, 0xec, 0x71, 0xcb, 0x23, 0x7f, 0xe6, 0x41, 0xc7, 0xee, 0x88, + 0x22, 0x4e, 0x16, 0xa6, 0x44, 0xa7, 0x67, 0xcf, 0xd9, 0x84, 0xe8, 0x13, 0xb1, 0xc9, 0xbd, 0x1b, + 0x8f, 0x1c, 0x26, 0xbf, 0x70, 0x9c, 0xa8, 0x9b, 0xf6, 0x7f, 0x38, 0x5e, 0x96, 0x27, 0xed, 0xbe, + 0x9e, 0x97, 0xb7, 0x3c, 0xf2, 0xa1, 0xfc, 0xa7, 0x8e, 0x0e, 0x68, 0x88, 0xa5, 0xd8, 0xca, 0xec, + 0xb2, 0xff, 0xfe, 0xb2, 0xed, 0xdd, 0xf2, 0xc8, 0x6f, 0xc9, 0xbf, 0x6d, 0xa8, 0x6f, 0x05, 0xd7, + 0xdf, 0xf4, 0x7b, 0xfa, 0xb6, 0x38, 0xc9, 0x55, 0x7a, 0xc9, 0x39, 0x49, 0x59, 0xb3, 0x1f, 0x00, + 0x14, 0xd1, 0x29, 0x29, 0x85, 0x6a, 0x46, 0xe7, 0x55, 0x03, 0x58, 0xf7, 0x36, 0x75, 0x44, 0x27, + 0xd5, 0x40, 0xc7, 0x8a, 0x0b, 0x73, 0x73, 0x9d, 0xd5, 0x28, 0xb3, 0xdf, 0xaf, 0x9b, 0x52, 0xf4, + 0xbf, 0x2a, 0xe8, 0xbf, 0x45, 0x2e, 0xdb, 0xf4, 0x77, 0x5e, 0xd8, 0x51, 0xe9, 0x4b, 0xf2, 0x39, + 0x74, 0xf7, 0x93, 0xe4, 0xd9, 0x34, 0x35, 0x09, 0x10, 0x37, 0xce, 0xc2, 0xc8, 0xb8, 0x5f, 0x3a, + 0x14, 0xbd, 0x2e, 0x28, 0x5f, 0x26, 0x97, 0x5c, 0xca, 0x45, 0xac, 0xfc, 0x92, 0x04, 0xb0, 0x66, + 0xec, 0x9d, 0x39, 0x48, 0xdf, 0xa5, 0x63, 0x87, 0xac, 0x95, 0x35, 0x1c, 0x0f, 0xc4, 0xac, 0x91, + 0x6b, 0x9a, 0xb7, 0x3c, 0x72, 0x00, 0x9d, 0x07, 0x6c, 0x98, 0x8c, 0x98, 0x0a, 0x8d, 0xd6, 0x8b, + 0x9d, 0x9b, 0x98, 0xaa, 0xdf, 0x75, 0x80, 0xae, 0x06, 0x48, 0x83, 0x59, 0xc6, 0xbe, 0xdc, 0x79, + 0xa1, 0x82, 0xae, 0x97, 0x5a, 0x03, 0xe8, 0x40, 0xd1, 0xd1, 0x00, 0xa5, 0xc8, 0xd2, 0xd1, 0x00, + 0x95, 0xc8, 0xd2, 0xd1, 0x00, 0x3a, 0x50, 0x25, 0x11, 0xc6, 0x9b, 0xa5, 0x60, 0xd4, 0x58, 0xcd, + 0xf3, 0x42, 0xd8, 0xfe, 0xb5, 0xf3, 0x11, 0xdc, 0xd5, 0x6e, 0xb8, 0xab, 0x1d, 0x42, 0xf7, 0x01, + 0x93, 0xcc, 0x92, 0x55, 0x85, 0xbe, 0xab, 0x52, 0xec, 0x0a, 0x44, 0x59, 0xdd, 0x88, 0x39, 0x57, + 0xc5, 0x8b, 0x94, 0x3e, 0xf9, 0x0e, 0xb4, 0x1f, 0x33, 0xae, 0xcb, 0x08, 0xc6, 0xf7, 0x28, 0xd5, + 0x15, 0xfa, 0x35, 0x55, 0x08, 0x7a, 0x4d, 0x50, 0xeb, 0x93, 0x9e, 0xa1, 0xb6, 0xc3, 0x46, 0x63, + 0x26, 0x1f, 0xff, 0x20, 0x1c, 0xbd, 0x24, 0xbf, 0x2e, 0x88, 0x9b, 0xca, 0xe3, 0xa6, 0x95, 0x7d, + 0xb6, 0x89, 0xaf, 0x94, 0xe0, 0x75, 0x94, 0xe3, 0x64, 0xc4, 0x2c, 0x63, 0x17, 0x43, 0xdb, 0x2a, + 0x33, 0x9b, 0x07, 0x55, 0xad, 0x5d, 0x9b, 0x07, 0x55, 0x53, 0x95, 0xa6, 0xdb, 0x62, 0x1d, 0x4a, + 0xae, 0x15, 0xeb, 0xc8, 0x4a, 0x74, 0xb1, 0xd2, 0xce, 0x8b, 0x60, 0xc2, 0x5f, 0x92, 0x2f, 0x44, + 0x6b, 0xb4, 0x5d, 0x2a, 0x29, 0x7c, 0x9f, 0x72, 0x55, 0xc5, 0x30, 0xcb, 0x9a, 0x72, 0xfd, 0x21, + 0xb9, 0x94, 0xb0, 0x89, 0x5f, 0x07, 0x38, 0xe4, 0x49, 0xfa, 0x20, 0x60, 0x93, 0x24, 0x2e, 0x34, + 0x59, 0x51, 0x0e, 0x28, 0x34, 0x99, 0x55, 0x13, 0x20, 0x5f, 0x58, 0xde, 0xa7, 0x53, 0x69, 0xd2, + 0xc2, 0x75, 0x6e, 0xc5, 0xc0, 0x30, 0xa4, 0xa6, 0x6a, 0x70, 0xcb, 0x43, 0x5f, 0xb2, 0x48, 0x7d, + 0x18, 0x5f, 0xb2, 0x92, 0x55, 0x31, 0x6a, 0xb0, 0x26, 0x4f, 0x72, 0x00, 0xad, 0x22, 0xfe, 0xd6, + 0xe6, 0xa9, 0x1c, 0xad, 0x1b, 0x7b, 0x53, 0x09, 0x8b, 0xe9, 0xaa, 0x60, 0x15, 0x90, 0x25, 0x64, + 0x95, 0xa8, 0x94, 0x0f, 0x61, 0x5d, 0x6e, 0xd0, 0x18, 0x4f, 0x91, 0xe0, 0xd6, 0x27, 0xa9, 0x09, + 0x83, 0xcd, 0x6b, 0xae, 0x8b, 0x22, 0xb5, 0x77, 0x42, 0xcd, 0x0a, 0x1f, 0x7a, 0x37, 0x8e, 0x16, + 0xc4, 0x1f, 0x8c, 0xbf, 0xf6, 0xdf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x8e, 0x28, 0xc4, 0x0c, 0x92, + 0x3c, 0x00, 0x00, } diff --git a/lnrpc/rpc.pb.gw.go b/lnrpc/rpc.pb.gw.go index e4b65da3..0c9fa72a 100644 --- a/lnrpc/rpc.pb.gw.go +++ b/lnrpc/rpc.pb.gw.go @@ -523,15 +523,15 @@ func request_Lightning_FeeReport_0(ctx context.Context, marshaler runtime.Marsha } -func request_Lightning_UpdateFees_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq FeeUpdateRequest +func request_Lightning_UpdateChannelPolicy_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq PolicyUpdateRequest var metadata runtime.ServerMetadata if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.UpdateFees(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.UpdateChannelPolicy(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } @@ -1452,7 +1452,7 @@ func RegisterLightningHandler(ctx context.Context, mux *runtime.ServeMux, conn * }) - mux.Handle("POST", pattern_Lightning_UpdateFees_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("POST", pattern_Lightning_UpdateChannelPolicy_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(ctx) defer cancel() if cn, ok := w.(http.CloseNotifier); ok { @@ -1470,14 +1470,14 @@ func RegisterLightningHandler(ctx context.Context, mux *runtime.ServeMux, conn * runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_Lightning_UpdateFees_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_Lightning_UpdateChannelPolicy_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_Lightning_UpdateFees_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_Lightning_UpdateChannelPolicy_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) @@ -1539,7 +1539,7 @@ var ( pattern_Lightning_FeeReport_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "fees"}, "")) - pattern_Lightning_UpdateFees_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "fees"}, "")) + pattern_Lightning_UpdateChannelPolicy_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "fees"}, "")) ) var ( @@ -1597,5 +1597,5 @@ var ( forward_Lightning_FeeReport_0 = runtime.ForwardResponseMessage - forward_Lightning_UpdateFees_0 = runtime.ForwardResponseMessage + forward_Lightning_UpdateChannelPolicy_0 = runtime.ForwardResponseMessage ) diff --git a/lnrpc/rpc.proto b/lnrpc/rpc.proto index 1598a9dd..6fca5c07 100644 --- a/lnrpc/rpc.proto +++ b/lnrpc/rpc.proto @@ -441,11 +441,11 @@ service Lightning { }; } - /** lncli: `updatefees` - UpdateFees allows the caller to update the fee schedule for all channels - globally, or a particular channel. + /** lncli: `updatechanpolicy` + UpdateChannelPolicy allows the caller to update the fee schedule and + channel policies for all channels globally, or a particular channel. */ - rpc UpdateFees(FeeUpdateRequest) returns (FeeUpdateResponse) { + rpc UpdateChannelPolicy(PolicyUpdateRequest) returns (PolicyUpdateResponse) { option (google.api.http) = { post: "/v1/fees" body: "*" @@ -1401,12 +1401,12 @@ message FeeReportResponse { repeated ChannelFeeReport channel_fees = 1 [json_name = "channel_fees"]; } -message FeeUpdateRequest { +message PolicyUpdateRequest { oneof scope { - /// If set, then this fee update applies to all currently active channels. + /// If set, then this update applies to all currently active channels. bool global = 1 [json_name = "global"] ; - /// If set, this fee update will target a specific channel. + /// If set, this update will target a specific channel. ChannelPoint chan_point = 2 [json_name = "chan_point"]; } @@ -1415,6 +1415,9 @@ message FeeUpdateRequest { /// The effective fee rate in milli-satoshis. The precision of this value goes up to 6 decimal places, so 1e-6. double fee_rate = 4 [json_name = "fee_rate"]; + + /// The required timelock delta for HTLCs forwarded over the channel. + uint32 time_lock_delta = 5 [json_name = "time_lock_delta"]; } -message FeeUpdateResponse { +message PolicyUpdateResponse { } diff --git a/lnrpc/rpc.swagger.json b/lnrpc/rpc.swagger.json index 53e34d59..aecdc2cf 100644 --- a/lnrpc/rpc.swagger.json +++ b/lnrpc/rpc.swagger.json @@ -222,13 +222,13 @@ ] }, "post": { - "summary": "* lncli: `updatefees`\nUpdateFees allows the caller to update the fee schedule for all channels\nglobally, or a particular channel.", - "operationId": "UpdateFees", + "summary": "* lncli: `updatechanpolicy`\nUpdateChannelPolicy allows the caller to update the fee schedule and\nchannel policies for all channels globally, or a particular channel.", + "operationId": "UpdateChannelPolicy", "responses": { "200": { "description": "", "schema": { - "$ref": "#/definitions/lnrpcFeeUpdateResponse" + "$ref": "#/definitions/lnrpcPolicyUpdateResponse" } } }, @@ -238,7 +238,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/lnrpcFeeUpdateRequest" + "$ref": "#/definitions/lnrpcPolicyUpdateRequest" } } ], @@ -1166,33 +1166,6 @@ } } }, - "lnrpcFeeUpdateRequest": { - "type": "object", - "properties": { - "global": { - "type": "boolean", - "format": "boolean", - "description": "/ If set, then this fee update applies to all currently active channels." - }, - "chan_point": { - "$ref": "#/definitions/lnrpcChannelPoint", - "description": "/ If set, this fee update will target a specific channel." - }, - "base_fee_msat": { - "type": "string", - "format": "int64", - "description": "/ The base fee charged regardless of the number of milli-satoshis sent." - }, - "fee_rate": { - "type": "number", - "format": "double", - "description": "/ The effective fee rate in milli-satoshis. The precision of this value goes up to 6 decimal places, so 1e-6." - } - } - }, - "lnrpcFeeUpdateResponse": { - "type": "object" - }, "lnrpcGetInfoResponse": { "type": "object", "properties": { @@ -1826,6 +1799,38 @@ } } }, + "lnrpcPolicyUpdateRequest": { + "type": "object", + "properties": { + "global": { + "type": "boolean", + "format": "boolean", + "description": "/ If set, then this update applies to all currently active channels." + }, + "chan_point": { + "$ref": "#/definitions/lnrpcChannelPoint", + "description": "/ If set, this update will target a specific channel." + }, + "base_fee_msat": { + "type": "string", + "format": "int64", + "description": "/ The base fee charged regardless of the number of milli-satoshis sent." + }, + "fee_rate": { + "type": "number", + "format": "double", + "description": "/ The effective fee rate in milli-satoshis. The precision of this value goes up to 6 decimal places, so 1e-6." + }, + "time_lock_delta": { + "type": "integer", + "format": "int64", + "description": "/ The required timelock delta for HTLCs forwarded over the channel." + } + } + }, + "lnrpcPolicyUpdateResponse": { + "type": "object" + }, "lnrpcQueryRoutesResponse": { "type": "object", "properties": { From 26421031e26056168afc350f4dfcb2ed8c68e86f Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sat, 16 Dec 2017 23:13:17 +0100 Subject: [PATCH 08/39] lncli: rename updatefees to updatechanpolicy This commit renames the `lncli updatefees` command to `lncli updatechanpolicy` and adds `time_lock_delta` as one of the passed parameters. --- cmd/lncli/commands.go | 59 ++++++++++++++++++++++++++++++------------- cmd/lncli/main.go | 2 +- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/cmd/lncli/commands.go b/cmd/lncli/commands.go index 43a40a11..94d511ed 100644 --- a/cmd/lncli/commands.go +++ b/cmd/lncli/commands.go @@ -1970,7 +1970,7 @@ var feeReportCommand = cli.Command{ Usage: "display the current fee policies of all active channels", Description: ` Returns the current fee policies of all active channels. - Fee policies can be updated using the updateFees command.`, + Fee policies can be updated using the updatechanpolicy command.`, Action: actionDecorator(feeReport), } @@ -1989,13 +1989,13 @@ func feeReport(ctx *cli.Context) error { return nil } -var updateFeesCommand = cli.Command{ - Name: "updatefees", - Usage: "update the fee policy for all channels, or a single channel", - ArgsUsage: "base_fee_msat fee_rate [channel_point]", +var updateChannelPolicyCommand = cli.Command{ + Name: "updatechanpolicy", + Usage: "update the channel policy for all channels, or a single channel", + ArgsUsage: "base_fee_msat fee_rate time_lock_delta [channel_point]", Description: ` - Updates the fee policy for all channels, or just a particular channel - identified by it's channel point. The fee update will be committed, and + Updates the channel policy for all channels, or just a particular channel + identified by its channel point. The update will be committed, and broadcast to the rest of the network within the next batch. Channel points are encoded as: funding_txid:output_index`, Flags: []cli.Flag{ @@ -2011,6 +2011,11 @@ var updateFeesCommand = cli.Command{ "proportionally based on the value of each " + "forwarded HTLC, the lowest possible rate is 0.000001", }, + cli.Int64Flag{ + Name: "time_lock_delta", + Usage: "the CLTV delta that will be applied to all " + + "forwarded HTLCs", + }, cli.StringFlag{ Name: "chan_point", Usage: "The channel whose fee policy should be " + @@ -2018,18 +2023,19 @@ var updateFeesCommand = cli.Command{ "will be updated. Takes the form of: txid:output_index", }, }, - Action: actionDecorator(updateFees), + Action: actionDecorator(updateChannelPolicy), } -func updateFees(ctx *cli.Context) error { +func updateChannelPolicy(ctx *cli.Context) error { ctxb := context.Background() client, cleanUp := getClient(ctx) defer cleanUp() var ( - baseFee int64 - feeRate float64 - err error + baseFee int64 + feeRate float64 + timeLockDelta int64 + err error ) args := ctx.Args() @@ -2060,10 +2066,26 @@ func updateFees(ctx *cli.Context) error { return fmt.Errorf("fee_rate argument missing") } + switch { + case ctx.IsSet("time_lock_delta"): + timeLockDelta = ctx.Int64("time_lock_delta") + case args.Present(): + timeLockDelta, err = strconv.ParseInt(args.First(), 10, 64) + if err != nil { + return fmt.Errorf("unable to decode time_lock_delta: %v", + err) + } + + args = args.Tail() + default: + return fmt.Errorf("time_lock_delta argument missing") + } + var ( chanPoint *lnrpc.ChannelPoint chanPointStr string ) + switch { case ctx.IsSet("chan_point"): chanPointStr = ctx.String("chan_point") @@ -2093,22 +2115,23 @@ func updateFees(ctx *cli.Context) error { } } - req := &lnrpc.FeeUpdateRequest{ - BaseFeeMsat: baseFee, - FeeRate: feeRate, + req := &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: baseFee, + FeeRate: feeRate, + TimeLockDelta: uint32(timeLockDelta), } if chanPoint != nil { - req.Scope = &lnrpc.FeeUpdateRequest_ChanPoint{ + req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{ ChanPoint: chanPoint, } } else { - req.Scope = &lnrpc.FeeUpdateRequest_Global{ + req.Scope = &lnrpc.PolicyUpdateRequest_Global{ Global: true, } } - resp, err := client.UpdateFees(ctxb, req) + resp, err := client.UpdateChannelPolicy(ctxb, req) if err != nil { return err } diff --git a/cmd/lncli/main.go b/cmd/lncli/main.go index 4344a28b..99a35b30 100644 --- a/cmd/lncli/main.go +++ b/cmd/lncli/main.go @@ -193,7 +193,7 @@ func main() { signMessageCommand, verifyMessageCommand, feeReportCommand, - updateFeesCommand, + updateChannelPolicyCommand, } if err := app.Run(os.Args); err != nil { From c4139b9f893a7861156b77b21aee07f6833c9b1a Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sat, 16 Dec 2017 23:14:58 +0100 Subject: [PATCH 09/39] rpcserver: rename UpdateFees -> UpdateChannelPolicy This commit changes the name of the UpdateFee method to UpdateChannelPolicy, to mimic the recent proto change. It also reads and validates the passed TimeLockDelta, and sends it to the gossiper for announcing it to the network, and to the switch for updating the forwarding policy of the links. --- rpcserver.go | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/rpcserver.go b/rpcserver.go index ac8b93ad..0f6495b8 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -3192,13 +3192,13 @@ func (r *rpcServer) FeeReport(ctx context.Context, // 0.000001, or 0.0001%. const minFeeRate = 1e-6 -// UpdateFees allows the caller to update the fee schedule for all channels -// globally, or a particular channel. -func (r *rpcServer) UpdateFees(ctx context.Context, - req *lnrpc.FeeUpdateRequest) (*lnrpc.FeeUpdateResponse, error) { +// UpdateChannelPolicy allows the caller to update the channel forwarding policy +// for all channels globally, or a particular channel. +func (r *rpcServer) UpdateChannelPolicy(ctx context.Context, + req *lnrpc.PolicyUpdateRequest) (*lnrpc.PolicyUpdateResponse, error) { if r.authSvc != nil { - if err := macaroons.ValidateMacaroon(ctx, "udpatefees", + if err := macaroons.ValidateMacaroon(ctx, "updatechannelpolicy", r.authSvc); err != nil { return nil, err } @@ -3208,11 +3208,11 @@ func (r *rpcServer) UpdateFees(ctx context.Context, switch scope := req.Scope.(type) { // If the request is targeting all active channels, then we don't need // target any channels by their channel point. - case *lnrpc.FeeUpdateRequest_Global: + case *lnrpc.PolicyUpdateRequest_Global: // Otherwise, we're targeting an individual channel by its channel // point. - case *lnrpc.FeeUpdateRequest_ChanPoint: + case *lnrpc.PolicyUpdateRequest_ChanPoint: txid, err := chainhash.NewHash(scope.ChanPoint.FundingTxid) if err != nil { return nil, err @@ -3226,12 +3226,19 @@ func (r *rpcServer) UpdateFees(ctx context.Context, } // As a sanity check, we'll ensure that the passed fee rate is below - // 1e-6, or the lowest allowed fee rate. + // 1e-6, or the lowest allowed fee rate, and that the passed timelock + // is large enough. if req.FeeRate < minFeeRate { return nil, fmt.Errorf("fee rate of %v is too small, min fee "+ "rate is %v", req.FeeRate, minFeeRate) } + if req.TimeLockDelta < minTimeLockDelta { + return nil, fmt.Errorf("time lock delta of %v is too small, "+ + "minimum supported is %v", req.TimeLockDelta, + minTimeLockDelta) + } + // We'll also need to convert the floating point fee rate we accept // over RPC to the fixed point rate that we use within the protocol. We // do this by multiplying the passed fee rate by the fee base. This @@ -3244,16 +3251,21 @@ func (r *rpcServer) UpdateFees(ctx context.Context, FeeRate: feeRateFixed, } - rpcsLog.Tracef("[updatefees] updating fee schedule base_fee=%v, "+ - "rate_float=%v, rate_fixed=%v, targets=%v", - req.BaseFeeMsat, req.FeeRate, feeRateFixed, + chanPolicy := routing.ChannelPolicy{ + FeeSchema: feeSchema, + TimeLockDelta: req.TimeLockDelta, + } + + rpcsLog.Tracef("[updatechanpolicy] updating channel policy base_fee=%v, "+ + "rate_float=%v, rate_fixed=%v, time_lock_delta: %v, targets=%v", + req.BaseFeeMsat, req.FeeRate, feeRateFixed, req.TimeLockDelta, spew.Sdump(targetChans)) // With the scope resolved, we'll now send this to the - // AuthenticatedGossiper so it can propagate the new fee schema for out + // AuthenticatedGossiper so it can propagate the new policy for our // target channel(s). - err := r.server.authGossiper.PropagateFeeUpdate( - feeSchema, targetChans..., + err := r.server.authGossiper.PropagateChanPolicyUpdate( + chanPolicy, targetChans..., ) if err != nil { return nil, err @@ -3265,8 +3277,9 @@ func (r *rpcServer) UpdateFees(ctx context.Context, // We create a partially policy as the logic won't overwrite a valid // sub-policy with a "nil" one. p := htlcswitch.ForwardingPolicy{ - BaseFee: baseFeeMsat, - FeeRate: lnwire.MilliSatoshi(feeRateFixed), + BaseFee: baseFeeMsat, + FeeRate: lnwire.MilliSatoshi(feeRateFixed), + TimeLockDelta: req.TimeLockDelta, } err = r.server.htlcSwitch.UpdateForwardingPolicies(p, targetChans...) if err != nil { @@ -3276,5 +3289,5 @@ func (r *rpcServer) UpdateFees(ctx context.Context, rpcsLog.Warnf("Unable to update link fees: %v", err) } - return &lnrpc.FeeUpdateResponse{}, nil + return &lnrpc.PolicyUpdateResponse{}, nil } From e27f524309145615a9f94ee3af17ab33e939cd06 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sat, 16 Dec 2017 23:58:08 +0100 Subject: [PATCH 10/39] lnrpc: add min_htlc_msat to OpenChanneRequest --- lnrpc/rpc.pb.go | 621 +++++++++++++++++++++-------------------- lnrpc/rpc.proto | 3 + lnrpc/rpc.swagger.json | 5 + 3 files changed, 323 insertions(+), 306 deletions(-) diff --git a/lnrpc/rpc.pb.go b/lnrpc/rpc.pb.go index 442ad099..6b22e50a 100644 --- a/lnrpc/rpc.pb.go +++ b/lnrpc/rpc.pb.go @@ -1555,6 +1555,8 @@ type OpenChannelRequest struct { SatPerByte int64 `protobuf:"varint,7,opt,name=sat_per_byte,json=satPerByte" json:"sat_per_byte,omitempty"` // / Whether this channel should be private, not announced to the greater network. Private bool `protobuf:"varint,8,opt,name=private" json:"private,omitempty"` + // / The minimum value in millisatoshi we will require for incoming HTLCs on the channel. + MinHtlcMsat int64 `protobuf:"varint,9,opt,name=min_htlc_msat" json:"min_htlc_msat,omitempty"` } func (m *OpenChannelRequest) Reset() { *m = OpenChannelRequest{} } @@ -1618,6 +1620,13 @@ func (m *OpenChannelRequest) GetPrivate() bool { return false } +func (m *OpenChannelRequest) GetMinHtlcMsat() int64 { + if m != nil { + return m.MinHtlcMsat + } + return 0 +} + type OpenStatusUpdate struct { // Types that are valid to be assigned to Update: // *OpenStatusUpdate_ChanPending @@ -5617,310 +5626,310 @@ var _Lightning_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("rpc.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 4867 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x7b, 0xcd, 0x8f, 0x1c, 0x49, - 0x56, 0xb8, 0xb3, 0xba, 0xfa, 0xa3, 0x5e, 0x55, 0xf5, 0x47, 0x74, 0xbb, 0xbb, 0x5c, 0xf6, 0x78, - 0xed, 0xd8, 0xd1, 0x4c, 0xff, 0xfc, 0x1b, 0xdc, 0x76, 0x2f, 0x3b, 0xcc, 0x8e, 0x81, 0x91, 0xbf, - 0x7b, 0xd8, 0x1e, 0x4f, 0x6f, 0xb6, 0x67, 0x06, 0x76, 0x85, 0x8a, 0xec, 0xaa, 0xe8, 0xea, 0x5c, - 0x67, 0x65, 0xe6, 0x64, 0x46, 0x75, 0xbb, 0xd6, 0x58, 0x82, 0x05, 0x21, 0x21, 0x81, 0xf6, 0x00, - 0x02, 0xed, 0x61, 0xb9, 0x70, 0x81, 0x03, 0x7f, 0x01, 0x12, 0x7f, 0xc0, 0x4a, 0x08, 0xa4, 0x3d, - 0x21, 0xb8, 0xc1, 0x09, 0xce, 0x1c, 0xb8, 0xa1, 0x17, 0x5f, 0x19, 0x91, 0x99, 0x6d, 0x7b, 0xd9, - 0x85, 0x5b, 0xc5, 0x8b, 0x97, 0x2f, 0x22, 0x5e, 0xbc, 0x78, 0xdf, 0x05, 0xad, 0x2c, 0x1d, 0xde, - 0x4c, 0xb3, 0x84, 0x27, 0x64, 0x3e, 0x8a, 0xb3, 0x74, 0xd8, 0xbf, 0x32, 0x4e, 0x92, 0x71, 0xc4, - 0x76, 0x82, 0x34, 0xdc, 0x09, 0xe2, 0x38, 0xe1, 0x01, 0x0f, 0x93, 0x38, 0x97, 0x48, 0xf4, 0x36, - 0xac, 0xdf, 0xcf, 0x58, 0xc0, 0xd9, 0x17, 0x41, 0x14, 0x31, 0xee, 0xb3, 0x2f, 0xa7, 0x2c, 0xe7, - 0xa4, 0x0f, 0x4b, 0x69, 0x90, 0xe7, 0x67, 0x49, 0x36, 0xea, 0x79, 0xd7, 0xbc, 0xed, 0x8e, 0x6f, - 0xc6, 0x74, 0x13, 0x36, 0xdc, 0x4f, 0xf2, 0x34, 0x89, 0x73, 0x86, 0xa4, 0x3e, 0x8b, 0xa3, 0x64, - 0xf8, 0xec, 0xa7, 0x22, 0xe5, 0x7e, 0xa2, 0x48, 0xfd, 0xb0, 0x01, 0xed, 0xa7, 0x59, 0x10, 0xe7, - 0xc1, 0x10, 0x37, 0x4b, 0x7a, 0xb0, 0xc8, 0x9f, 0x0f, 0x4e, 0x82, 0xfc, 0x44, 0x90, 0x68, 0xf9, - 0x7a, 0x48, 0x36, 0x61, 0x21, 0x98, 0x24, 0xd3, 0x98, 0xf7, 0x1a, 0xd7, 0xbc, 0xed, 0x39, 0x5f, - 0x8d, 0xc8, 0x7b, 0xb0, 0x16, 0x4f, 0x27, 0x83, 0x61, 0x12, 0x1f, 0x87, 0xd9, 0x44, 0x1e, 0xb9, - 0x37, 0x77, 0xcd, 0xdb, 0x9e, 0xf7, 0xab, 0x13, 0xe4, 0x2a, 0xc0, 0x11, 0x6e, 0x43, 0x2e, 0xd1, - 0x14, 0x4b, 0x58, 0x10, 0x42, 0xa1, 0xa3, 0x46, 0x2c, 0x1c, 0x9f, 0xf0, 0xde, 0xbc, 0x20, 0xe4, - 0xc0, 0x90, 0x06, 0x0f, 0x27, 0x6c, 0x90, 0xf3, 0x60, 0x92, 0xf6, 0x16, 0xc4, 0x6e, 0x2c, 0x88, - 0x98, 0x4f, 0x78, 0x10, 0x0d, 0x8e, 0x19, 0xcb, 0x7b, 0x8b, 0x6a, 0xde, 0x40, 0xc8, 0x3b, 0xb0, - 0x3c, 0x62, 0x39, 0x1f, 0x04, 0xa3, 0x51, 0xc6, 0xf2, 0x9c, 0xe5, 0xbd, 0xa5, 0x6b, 0x73, 0xdb, - 0x2d, 0xbf, 0x04, 0xa5, 0x3d, 0xd8, 0x7c, 0xcc, 0xb8, 0xc5, 0x9d, 0x5c, 0x71, 0x9a, 0xee, 0x03, - 0xb1, 0xc0, 0x0f, 0x18, 0x0f, 0xc2, 0x28, 0x27, 0xef, 0x43, 0x87, 0x5b, 0xc8, 0x3d, 0xef, 0xda, - 0xdc, 0x76, 0x7b, 0x97, 0xdc, 0x14, 0xd2, 0x71, 0xd3, 0xfa, 0xc0, 0x77, 0xf0, 0xe8, 0x3f, 0x7a, - 0xd0, 0x3e, 0x64, 0xf1, 0x48, 0xdf, 0x23, 0x81, 0x26, 0xee, 0x44, 0xdd, 0xa1, 0xf8, 0x4d, 0xbe, - 0x02, 0x6d, 0xb1, 0xbb, 0x9c, 0x67, 0x61, 0x3c, 0x16, 0x57, 0xd0, 0xf2, 0x01, 0x41, 0x87, 0x02, - 0x42, 0x56, 0x61, 0x2e, 0x98, 0x70, 0xc1, 0xf8, 0x39, 0x1f, 0x7f, 0x92, 0xeb, 0xd0, 0x49, 0x83, - 0xd9, 0x84, 0xc5, 0xbc, 0x60, 0x76, 0xc7, 0x6f, 0x2b, 0xd8, 0x1e, 0x72, 0xfb, 0x26, 0xac, 0xdb, - 0x28, 0x9a, 0xfa, 0xbc, 0xa0, 0xbe, 0x66, 0x61, 0xaa, 0x45, 0xde, 0x85, 0x15, 0x8d, 0x9f, 0xc9, - 0xcd, 0x0a, 0xf6, 0xb7, 0xfc, 0x65, 0x05, 0xd6, 0x0c, 0xfa, 0x53, 0x0f, 0x3a, 0xf2, 0x48, 0x52, - 0xce, 0xc8, 0xdb, 0xd0, 0xd5, 0x5f, 0xb2, 0x2c, 0x4b, 0x32, 0x25, 0x5d, 0x2e, 0x90, 0xdc, 0x80, - 0x55, 0x0d, 0x48, 0x33, 0x16, 0x4e, 0x82, 0x31, 0x13, 0x47, 0xed, 0xf8, 0x15, 0x38, 0xd9, 0x2d, - 0x28, 0x66, 0xc9, 0x94, 0x33, 0x71, 0xf4, 0xf6, 0x6e, 0x47, 0xb1, 0xdb, 0x47, 0x98, 0xef, 0xa2, - 0xd0, 0xef, 0x7b, 0xd0, 0xb9, 0x7f, 0x12, 0xc4, 0x31, 0x8b, 0x0e, 0x92, 0x30, 0xe6, 0x28, 0x6e, - 0xc7, 0xd3, 0x78, 0x14, 0xc6, 0xe3, 0x01, 0x7f, 0x1e, 0xea, 0x67, 0xe3, 0xc0, 0x70, 0x53, 0xf6, - 0x18, 0x99, 0xa4, 0xf8, 0x5f, 0x81, 0x23, 0xbd, 0x64, 0xca, 0xd3, 0x29, 0x1f, 0x84, 0xf1, 0x88, - 0x3d, 0x17, 0x7b, 0xea, 0xfa, 0x0e, 0x8c, 0xfe, 0x2a, 0xac, 0xee, 0xa3, 0x1c, 0xc7, 0x61, 0x3c, - 0xbe, 0x2b, 0x85, 0x0d, 0x1f, 0x57, 0x3a, 0x3d, 0x7a, 0xc6, 0x66, 0x8a, 0x2f, 0x6a, 0x84, 0xa2, - 0x70, 0x92, 0xe4, 0x5c, 0xad, 0x27, 0x7e, 0xd3, 0x7f, 0xf5, 0x60, 0x05, 0x79, 0xfb, 0x49, 0x10, - 0xcf, 0xb4, 0xc8, 0xec, 0x43, 0x07, 0x49, 0x3d, 0x4d, 0xee, 0xca, 0x27, 0x2a, 0x45, 0x6f, 0x5b, - 0xf1, 0xa2, 0x84, 0x7d, 0xd3, 0x46, 0x7d, 0x18, 0xf3, 0x6c, 0xe6, 0x3b, 0x5f, 0xa3, 0xb0, 0xf1, - 0x20, 0x1b, 0x33, 0x2e, 0x1e, 0xaf, 0x7a, 0xcc, 0x20, 0x41, 0xf7, 0x93, 0xf8, 0x98, 0x5c, 0x83, - 0x4e, 0x1e, 0xf0, 0x41, 0xca, 0xb2, 0xc1, 0xd1, 0x8c, 0x33, 0x21, 0x30, 0x73, 0x3e, 0xe4, 0x01, - 0x3f, 0x60, 0xd9, 0xbd, 0x19, 0x67, 0xfd, 0x8f, 0x60, 0xad, 0xb2, 0x0a, 0xca, 0x68, 0x71, 0x44, - 0xfc, 0x49, 0x36, 0x60, 0xfe, 0x34, 0x88, 0xa6, 0x4c, 0xe9, 0x14, 0x39, 0xf8, 0xb0, 0xf1, 0x81, - 0x47, 0xdf, 0x81, 0xd5, 0x62, 0xdb, 0x4a, 0x88, 0x08, 0x34, 0xcd, 0x2d, 0xb5, 0x7c, 0xf1, 0x9b, - 0xfe, 0xae, 0x27, 0x11, 0xef, 0x27, 0xa1, 0x79, 0x9f, 0x88, 0x88, 0xcf, 0x58, 0x23, 0xe2, 0xef, - 0x73, 0xf5, 0xd7, 0xcf, 0x7e, 0x58, 0xfa, 0x2e, 0xac, 0x59, 0x5b, 0x78, 0xc5, 0x66, 0xff, 0xc2, - 0x83, 0xb5, 0x27, 0xec, 0x4c, 0xdd, 0xba, 0xde, 0xed, 0x07, 0xd0, 0xe4, 0xb3, 0x94, 0x09, 0xcc, - 0xe5, 0xdd, 0xb7, 0xd5, 0xa5, 0x55, 0xf0, 0x6e, 0xaa, 0xe1, 0xd3, 0x59, 0xca, 0x7c, 0xf1, 0x05, - 0xfd, 0x14, 0xda, 0x16, 0x90, 0x6c, 0xc1, 0xfa, 0x17, 0x1f, 0x3f, 0x7d, 0xf2, 0xf0, 0xf0, 0x70, - 0x70, 0xf0, 0xd9, 0xbd, 0x6f, 0x3e, 0xfc, 0x8d, 0xc1, 0xde, 0xdd, 0xc3, 0xbd, 0xd5, 0x0b, 0x64, - 0x13, 0xc8, 0x93, 0x87, 0x87, 0x4f, 0x1f, 0x3e, 0x70, 0xe0, 0x1e, 0x59, 0x81, 0xb6, 0x0d, 0x68, - 0xd0, 0x3e, 0xf4, 0x9e, 0xb0, 0xb3, 0x2f, 0x42, 0x1e, 0xb3, 0x3c, 0x77, 0x97, 0xa7, 0x37, 0x81, - 0xd8, 0x7b, 0x52, 0xc7, 0xec, 0xc1, 0xa2, 0xd2, 0x98, 0xda, 0x60, 0xa8, 0x21, 0x7d, 0x07, 0xc8, - 0x61, 0x38, 0x8e, 0x3f, 0x61, 0x79, 0x1e, 0x8c, 0x99, 0x3e, 0xec, 0x2a, 0xcc, 0x4d, 0xf2, 0xb1, - 0x7a, 0x68, 0xf8, 0x93, 0x7e, 0x0d, 0xd6, 0x1d, 0x3c, 0x45, 0xf8, 0x0a, 0xb4, 0xf2, 0x70, 0x1c, - 0x07, 0x7c, 0x9a, 0x31, 0x45, 0xba, 0x00, 0xd0, 0x47, 0xb0, 0xf1, 0x39, 0xcb, 0xc2, 0xe3, 0xd9, - 0xeb, 0xc8, 0xbb, 0x74, 0x1a, 0x65, 0x3a, 0x0f, 0xe1, 0x62, 0x89, 0x8e, 0x5a, 0x5e, 0x4a, 0xa6, - 0xba, 0xbf, 0x25, 0x5f, 0x0e, 0xac, 0x77, 0xda, 0xb0, 0xdf, 0x29, 0xfd, 0x0c, 0xc8, 0xfd, 0x24, - 0x8e, 0xd9, 0x90, 0x1f, 0x30, 0x96, 0xe9, 0xcd, 0xfc, 0x7f, 0x4b, 0x0c, 0xdb, 0xbb, 0x5b, 0xea, - 0x62, 0xcb, 0x8f, 0x5f, 0xc9, 0x27, 0x81, 0x66, 0xca, 0xb2, 0x89, 0x20, 0xbc, 0xe4, 0x8b, 0xdf, - 0x74, 0x07, 0xd6, 0x1d, 0xb2, 0x05, 0xcf, 0x53, 0xc6, 0xb2, 0x81, 0xda, 0xdd, 0xbc, 0xaf, 0x87, - 0xf4, 0x36, 0x5c, 0x7c, 0x10, 0xe6, 0xc3, 0xea, 0x56, 0xf0, 0x93, 0xe9, 0xd1, 0xa0, 0x78, 0x7e, - 0x7a, 0x88, 0x56, 0xae, 0xfc, 0x89, 0xf2, 0x0d, 0xfe, 0xc0, 0x83, 0xe6, 0xde, 0xd3, 0xfd, 0xfb, - 0xe8, 0x58, 0x84, 0xf1, 0x30, 0x99, 0xa0, 0x6d, 0x90, 0xec, 0x30, 0xe3, 0x73, 0x9f, 0xd5, 0x15, - 0x68, 0x09, 0x93, 0x82, 0x86, 0x5b, 0x3c, 0xaa, 0x8e, 0x5f, 0x00, 0xd0, 0x69, 0x60, 0xcf, 0xd3, - 0x30, 0x13, 0x5e, 0x81, 0xb6, 0xf5, 0x4d, 0xa1, 0x2c, 0xab, 0x13, 0xf4, 0xdf, 0x9b, 0xd0, 0xbd, - 0x3b, 0xe4, 0xe1, 0x29, 0x53, 0xca, 0x5b, 0xac, 0x2a, 0x00, 0x6a, 0x3f, 0x6a, 0x84, 0x66, 0x26, - 0x63, 0x93, 0x84, 0xb3, 0x81, 0x73, 0x4d, 0x2e, 0x10, 0xb1, 0x86, 0x92, 0xd0, 0x20, 0x45, 0x33, - 0x20, 0xf6, 0xd7, 0xf2, 0x5d, 0x20, 0xb2, 0x0c, 0x01, 0xc8, 0x65, 0xdc, 0x59, 0xd3, 0xd7, 0x43, - 0xe4, 0xc7, 0x30, 0x48, 0x83, 0x61, 0xc8, 0x67, 0x4a, 0x1b, 0x98, 0x31, 0xd2, 0x8e, 0x92, 0x61, - 0x10, 0x0d, 0x8e, 0x82, 0x28, 0x88, 0x87, 0x4c, 0xf9, 0x27, 0x2e, 0x10, 0x5d, 0x10, 0xb5, 0x25, - 0x8d, 0x26, 0xdd, 0x94, 0x12, 0x14, 0x5d, 0x99, 0x61, 0x32, 0x99, 0x84, 0x1c, 0x3d, 0x97, 0xde, - 0x92, 0xd4, 0x3c, 0x05, 0x44, 0x9c, 0x44, 0x8e, 0xce, 0x24, 0x0f, 0x5b, 0x72, 0x35, 0x07, 0x88, - 0x54, 0x8e, 0x19, 0x13, 0x1a, 0xec, 0xd9, 0x59, 0x0f, 0x24, 0x95, 0x02, 0x82, 0xb7, 0x31, 0x8d, - 0x73, 0xc6, 0x79, 0xc4, 0x46, 0x66, 0x43, 0x6d, 0x81, 0x56, 0x9d, 0x20, 0xb7, 0x60, 0x5d, 0x3a, - 0x53, 0x79, 0xc0, 0x93, 0xfc, 0x24, 0xcc, 0x07, 0x39, 0x8b, 0x79, 0xaf, 0x23, 0xf0, 0xeb, 0xa6, - 0xc8, 0x07, 0xb0, 0x55, 0x02, 0x67, 0x6c, 0xc8, 0xc2, 0x53, 0x36, 0xea, 0x75, 0xc5, 0x57, 0xe7, - 0x4d, 0x93, 0x6b, 0xd0, 0x46, 0x1f, 0x72, 0x9a, 0x8e, 0x02, 0xce, 0xf2, 0xde, 0xb2, 0xb8, 0x07, - 0x1b, 0x44, 0x6e, 0x43, 0x37, 0x65, 0xd2, 0x0a, 0x9f, 0xf0, 0x68, 0x98, 0xf7, 0x56, 0x84, 0xe9, - 0x6b, 0xab, 0xc7, 0x86, 0xf2, 0xeb, 0xbb, 0x18, 0x28, 0x9a, 0xc3, 0xfc, 0x74, 0x30, 0x62, 0x51, - 0x30, 0xeb, 0xad, 0x0a, 0xa1, 0x2b, 0x00, 0xf4, 0x22, 0xac, 0xef, 0x87, 0x39, 0x57, 0x92, 0x66, - 0xb4, 0xdf, 0x1e, 0x6c, 0xb8, 0x60, 0xf5, 0x16, 0x6f, 0xc1, 0x92, 0x12, 0x9b, 0xbc, 0xd7, 0x16, - 0x4b, 0x6f, 0xa8, 0xa5, 0x1d, 0x89, 0xf5, 0x0d, 0x16, 0xfd, 0xfd, 0x06, 0x34, 0xf1, 0x9d, 0x9d, - 0xff, 0x26, 0xed, 0x07, 0xde, 0x70, 0x1e, 0xb8, 0xad, 0x6e, 0xe7, 0x1c, 0x75, 0x2b, 0x3c, 0xeb, - 0x19, 0x67, 0xea, 0x36, 0xa4, 0xc4, 0x5a, 0x90, 0x62, 0x3e, 0x63, 0xc3, 0x53, 0x21, 0xb6, 0x66, - 0x1e, 0x21, 0x28, 0xd4, 0x68, 0xe6, 0xc4, 0xd7, 0x52, 0x66, 0xcd, 0x58, 0xcf, 0x89, 0x2f, 0x17, - 0x8b, 0x39, 0xf1, 0x5d, 0x0f, 0x16, 0xc3, 0xf8, 0x28, 0x99, 0xc6, 0x23, 0x21, 0x9f, 0x4b, 0xbe, - 0x1e, 0x22, 0x9f, 0x53, 0xe1, 0x1d, 0x85, 0x13, 0xa6, 0x04, 0xb3, 0x00, 0x50, 0x82, 0x6e, 0x50, - 0x2e, 0x34, 0x8e, 0x61, 0xf2, 0xfb, 0xb0, 0x66, 0xc1, 0x14, 0x87, 0xaf, 0xc3, 0x3c, 0x9e, 0x5e, - 0xfb, 0xd3, 0xfa, 0x66, 0x85, 0xaa, 0x92, 0x33, 0x74, 0x15, 0x96, 0x1f, 0x33, 0xfe, 0x71, 0x7c, - 0x9c, 0x68, 0x4a, 0x7f, 0x38, 0x07, 0x2b, 0x06, 0xa4, 0x08, 0x6d, 0xc3, 0x4a, 0x38, 0x62, 0x31, - 0x0f, 0xf9, 0x6c, 0xe0, 0x78, 0x5b, 0x65, 0x30, 0x2a, 0xff, 0x20, 0x0a, 0x83, 0x5c, 0xa9, 0x0f, - 0x39, 0x20, 0xbb, 0xb0, 0x81, 0x92, 0xa7, 0x85, 0xc9, 0x5c, 0xbb, 0x74, 0xf2, 0x6a, 0xe7, 0xf0, - 0xb1, 0x20, 0x5c, 0xaa, 0xa7, 0xe2, 0x13, 0xa9, 0xea, 0xea, 0xa6, 0x90, 0x6b, 0x92, 0x12, 0x1e, - 0x79, 0x5e, 0x4a, 0xa7, 0x01, 0x54, 0xe2, 0xa3, 0x05, 0xe9, 0x60, 0x96, 0xe3, 0x23, 0x2b, 0xc6, - 0x5a, 0xaa, 0xc4, 0x58, 0xdb, 0xb0, 0x92, 0xcf, 0xe2, 0x21, 0x1b, 0x0d, 0x78, 0x82, 0xeb, 0x86, - 0xb1, 0xb8, 0x9d, 0x25, 0xbf, 0x0c, 0x16, 0xd1, 0x20, 0xcb, 0x79, 0xcc, 0xb8, 0xd0, 0x1a, 0x4b, - 0xbe, 0x1e, 0xa2, 0x02, 0x16, 0x28, 0x52, 0xe8, 0x5b, 0xbe, 0x1a, 0xa1, 0x15, 0x9b, 0x66, 0x61, - 0xde, 0xeb, 0x08, 0xa8, 0xf8, 0x4d, 0xbf, 0x27, 0x8c, 0xa3, 0x09, 0x02, 0x3f, 0x13, 0x2f, 0x97, - 0x5c, 0x86, 0x96, 0xdc, 0x53, 0x7e, 0x12, 0xe8, 0x70, 0x55, 0x00, 0x0e, 0x4f, 0x02, 0x8c, 0x5d, - 0x9c, 0x63, 0xca, 0x57, 0xd0, 0x16, 0xb0, 0x3d, 0x79, 0xca, 0xb7, 0x61, 0x59, 0x87, 0x97, 0xf9, - 0x20, 0x62, 0xc7, 0x5c, 0x3b, 0xdb, 0xf1, 0x74, 0x82, 0xcb, 0xe5, 0xfb, 0xec, 0x98, 0xd3, 0x27, - 0xb0, 0xa6, 0x5e, 0xe0, 0xa7, 0x29, 0xd3, 0x4b, 0x7f, 0xa3, 0xac, 0xff, 0xa5, 0x81, 0x5e, 0x57, - 0x92, 0x65, 0x47, 0x08, 0x25, 0xa3, 0x40, 0x7d, 0x20, 0x6a, 0xfa, 0x7e, 0x94, 0xe4, 0x4c, 0x11, - 0xa4, 0xd0, 0x19, 0x46, 0x49, 0x5e, 0x0e, 0x23, 0x6c, 0x18, 0xf2, 0x32, 0x9f, 0x0e, 0x87, 0xf8, - 0x72, 0xa5, 0x89, 0xd7, 0x43, 0xfa, 0x57, 0x1e, 0xac, 0x0b, 0x6a, 0x5a, 0x57, 0x18, 0xbf, 0xf0, - 0xcd, 0xb7, 0xd9, 0x19, 0xda, 0x61, 0xcd, 0x06, 0xcc, 0x1f, 0x27, 0xd9, 0x90, 0xa9, 0x95, 0xe4, - 0xe0, 0xe7, 0xe1, 0xe9, 0xfe, 0x93, 0x07, 0x6b, 0x62, 0xab, 0x87, 0x3c, 0xe0, 0xd3, 0x5c, 0x1d, - 0xff, 0x97, 0xa1, 0x8b, 0x47, 0x65, 0x5a, 0xfc, 0xd5, 0x46, 0x37, 0xcc, 0x4b, 0x15, 0x50, 0x89, - 0xbc, 0x77, 0xc1, 0x77, 0x91, 0xc9, 0x47, 0xd0, 0xb1, 0x73, 0x04, 0x62, 0xcf, 0xed, 0xdd, 0x4b, - 0xfa, 0x94, 0x15, 0xc9, 0xd9, 0xbb, 0xe0, 0x3b, 0x1f, 0x90, 0x3b, 0x00, 0xc2, 0x32, 0x0b, 0xb2, - 0x2a, 0x0c, 0xbc, 0xe4, 0x32, 0xc9, 0xba, 0xac, 0xbd, 0x0b, 0xbe, 0x85, 0x7e, 0x6f, 0x09, 0x16, - 0xa4, 0x29, 0xa1, 0x8f, 0xa1, 0xeb, 0xec, 0xd4, 0xf1, 0xe0, 0x3b, 0xd2, 0x83, 0xaf, 0x04, 0x78, - 0x8d, 0x9a, 0x00, 0xef, 0x6f, 0x1b, 0x40, 0x50, 0xda, 0x4a, 0xd7, 0xf9, 0x0e, 0x2c, 0x2b, 0xf6, - 0xbb, 0xce, 0x5b, 0x09, 0x2a, 0x6c, 0x5e, 0x32, 0x72, 0x3c, 0x98, 0x8e, 0x6f, 0x83, 0xc8, 0x4d, - 0x20, 0xd6, 0x50, 0x47, 0xed, 0xd2, 0x1e, 0xd4, 0xcc, 0xa0, 0xe2, 0x92, 0xee, 0x87, 0x8e, 0x57, - 0x95, 0xc7, 0xd6, 0x14, 0xf7, 0x5b, 0x3b, 0x27, 0x92, 0x49, 0xd3, 0xfc, 0x04, 0x6d, 0xb2, 0xf6, - 0x71, 0xf4, 0xb8, 0x2c, 0x48, 0x0b, 0xaf, 0x15, 0xa4, 0xc5, 0xb2, 0x20, 0x09, 0x0b, 0x97, 0x85, - 0xa7, 0x01, 0x67, 0xda, 0x6a, 0xa8, 0x21, 0xfd, 0x89, 0x07, 0xab, 0xc8, 0x3d, 0x47, 0xc2, 0x3e, - 0x04, 0x21, 0xe0, 0x6f, 0x28, 0x60, 0x0e, 0xee, 0xcf, 0x2e, 0x5f, 0x1f, 0x40, 0x4b, 0x10, 0x4c, - 0x52, 0x16, 0x2b, 0xf1, 0xea, 0xb9, 0xe2, 0x55, 0xe8, 0x96, 0xbd, 0x0b, 0x7e, 0x81, 0x6c, 0x09, - 0xd7, 0x3f, 0x78, 0xd0, 0x56, 0xdb, 0xfc, 0x1f, 0xbb, 0xd4, 0x7d, 0x58, 0x42, 0x39, 0xb3, 0x3c, - 0x56, 0x33, 0x46, 0x9d, 0x3e, 0xc1, 0x88, 0x06, 0x8d, 0x98, 0xe3, 0x4e, 0x97, 0xc1, 0x68, 0x91, - 0x84, 0x1a, 0xcd, 0x07, 0x3c, 0x8c, 0x06, 0x7a, 0x56, 0x25, 0xda, 0xea, 0xa6, 0x50, 0x9b, 0xe4, - 0x3c, 0x18, 0x33, 0x65, 0x6c, 0xe4, 0x00, 0xe3, 0x06, 0x75, 0xa0, 0xb2, 0xab, 0xf4, 0x63, 0x80, - 0xad, 0xca, 0x94, 0x71, 0x97, 0x94, 0x87, 0x18, 0x85, 0x93, 0xa3, 0xc4, 0x38, 0x9b, 0x9e, 0xed, - 0x3c, 0x3a, 0x53, 0x64, 0x0c, 0x17, 0xb5, 0x55, 0x45, 0x9e, 0x16, 0x36, 0xb4, 0x21, 0xdc, 0x81, - 0xdb, 0xae, 0x0c, 0x94, 0x17, 0xd4, 0x70, 0xfb, 0x3d, 0xd6, 0xd3, 0x23, 0x27, 0xd0, 0x33, 0xe6, - 0x5b, 0x29, 0x6e, 0xcb, 0xc4, 0xe3, 0x5a, 0xef, 0xbd, 0x66, 0x2d, 0xa1, 0x65, 0x46, 0x7a, 0x99, - 0x73, 0xa9, 0x91, 0x19, 0x5c, 0xd5, 0x73, 0x42, 0x33, 0x57, 0xd7, 0x6b, 0xbe, 0xd1, 0xd9, 0x1e, - 0xe1, 0xc7, 0xee, 0xa2, 0xaf, 0x21, 0xdc, 0xff, 0xb1, 0x07, 0xcb, 0x2e, 0x39, 0x14, 0x1d, 0x15, - 0x75, 0x68, 0xd5, 0xa1, 0xdd, 0xa2, 0x12, 0xb8, 0x1a, 0x37, 0x35, 0xea, 0xe2, 0x26, 0x3b, 0x3a, - 0x9a, 0x7b, 0x5d, 0x74, 0xd4, 0x7c, 0xb3, 0xe8, 0x68, 0xbe, 0x2e, 0x3a, 0xea, 0xff, 0xa7, 0x07, - 0xa4, 0x7a, 0xbf, 0xe4, 0xb1, 0x0c, 0xdc, 0x62, 0x16, 0x29, 0x3d, 0xf1, 0x0b, 0x6f, 0x26, 0x23, - 0x9a, 0x87, 0xfa, 0x6b, 0x14, 0x56, 0x5b, 0x11, 0xd8, 0xce, 0x48, 0xd7, 0xaf, 0x9b, 0x2a, 0xc5, - 0x6b, 0xcd, 0xd7, 0xc7, 0x6b, 0xf3, 0xaf, 0x8f, 0xd7, 0x16, 0xca, 0xf1, 0x5a, 0xff, 0xb7, 0xa1, - 0xeb, 0xdc, 0xfa, 0xcf, 0xef, 0xc4, 0x65, 0x47, 0x46, 0x5e, 0xb0, 0x03, 0xeb, 0xff, 0x47, 0x03, - 0x48, 0x55, 0xf2, 0xfe, 0x4f, 0xf7, 0x20, 0xe4, 0xc8, 0x51, 0x20, 0x73, 0x4a, 0x8e, 0x1c, 0xd5, - 0xf1, 0xbf, 0xa9, 0x14, 0xdf, 0x83, 0xb5, 0x8c, 0x0d, 0x93, 0x53, 0x96, 0x59, 0x31, 0xb3, 0xbc, - 0xaa, 0xea, 0x04, 0xba, 0x72, 0x6e, 0x94, 0xba, 0xe4, 0xd4, 0x06, 0x2c, 0xcb, 0x50, 0x0a, 0x56, - 0xe9, 0x37, 0x60, 0x43, 0x96, 0x6c, 0xee, 0x49, 0x52, 0xda, 0x9b, 0xb8, 0x0e, 0x9d, 0x33, 0x99, - 0xa6, 0x1b, 0x24, 0x71, 0x34, 0x53, 0x46, 0xa4, 0xad, 0x60, 0x9f, 0xc6, 0xd1, 0x8c, 0xfe, 0xc8, - 0x83, 0x8b, 0xa5, 0x6f, 0x8b, 0x6c, 0xbc, 0x54, 0xb5, 0xae, 0xfe, 0x75, 0x81, 0x78, 0x44, 0x25, - 0xe3, 0xd6, 0x11, 0xa5, 0x49, 0xaa, 0x4e, 0x20, 0x0b, 0xa7, 0x71, 0x15, 0x5f, 0x5e, 0x4c, 0xdd, - 0x14, 0xdd, 0x82, 0x8b, 0xea, 0xf2, 0xdd, 0xb3, 0xd1, 0x5d, 0xd8, 0x2c, 0x4f, 0x14, 0x99, 0x2f, - 0x77, 0xcb, 0x7a, 0x48, 0x3f, 0x02, 0xf2, 0xad, 0x29, 0xcb, 0x66, 0x22, 0xef, 0x6f, 0x52, 0xab, - 0x5b, 0xe5, 0x10, 0x7b, 0x21, 0x9d, 0x1e, 0x7d, 0x93, 0xcd, 0x74, 0xb9, 0xa4, 0x61, 0xca, 0x25, - 0xf4, 0x0e, 0xac, 0x3b, 0x04, 0x0c, 0xab, 0x16, 0x44, 0xed, 0x40, 0x87, 0x9f, 0x6e, 0x7d, 0x41, - 0xcd, 0xd1, 0x3f, 0xf7, 0x60, 0x6e, 0x2f, 0x49, 0xed, 0x9c, 0x91, 0xe7, 0xe6, 0x8c, 0x94, 0xee, - 0x1c, 0x18, 0xd5, 0xd8, 0x50, 0x2f, 0xdf, 0x06, 0xa2, 0xe6, 0x0b, 0x26, 0x1c, 0x03, 0xb0, 0xe3, - 0x24, 0x3b, 0x0b, 0xb2, 0x91, 0xe2, 0x5f, 0x09, 0x8a, 0xdb, 0x2f, 0x14, 0x0c, 0xfe, 0x44, 0xa7, - 0x41, 0x24, 0xce, 0x66, 0x2a, 0x66, 0x54, 0x23, 0xfa, 0x03, 0x0f, 0xe6, 0xc5, 0x5e, 0xf1, 0x35, - 0xc8, 0xfb, 0x15, 0xa5, 0x32, 0x91, 0x97, 0xf3, 0xe4, 0x6b, 0x28, 0x81, 0x4b, 0x05, 0xb4, 0x46, - 0xa5, 0x80, 0x76, 0x05, 0x5a, 0x72, 0x54, 0x54, 0x9c, 0x0a, 0x00, 0xb9, 0x0a, 0xcd, 0x93, 0x24, - 0xd5, 0x36, 0x0c, 0x74, 0x22, 0x26, 0x49, 0x7d, 0x01, 0xa7, 0x37, 0x60, 0xe5, 0x49, 0x32, 0x62, - 0x56, 0xb4, 0x7e, 0xee, 0x35, 0xd1, 0xdf, 0xf1, 0x60, 0x49, 0x23, 0x93, 0x6d, 0x68, 0xa2, 0x29, - 0x2a, 0x39, 0x7f, 0x26, 0x9d, 0x8a, 0x78, 0xbe, 0xc0, 0x40, 0x15, 0x22, 0x62, 0xc3, 0xc2, 0x55, - 0xd0, 0x91, 0x61, 0x61, 0x84, 0xd1, 0x1d, 0x17, 0x7b, 0x2e, 0x19, 0xab, 0x12, 0x94, 0xfe, 0xb5, - 0x07, 0x5d, 0x67, 0x0d, 0x74, 0xd0, 0xa3, 0x20, 0xe7, 0x2a, 0x05, 0xa5, 0x98, 0x68, 0x83, 0xec, - 0xcc, 0x4e, 0xc3, 0xcd, 0xec, 0x98, 0xcc, 0xc2, 0x9c, 0x9d, 0x59, 0xb8, 0x05, 0xad, 0xa2, 0x18, - 0xd9, 0x74, 0x54, 0x03, 0xae, 0xa8, 0x13, 0xc5, 0x05, 0x12, 0xd2, 0x19, 0x26, 0x51, 0x92, 0xa9, - 0x5a, 0x9d, 0x1c, 0xd0, 0x3b, 0xd0, 0xb6, 0xf0, 0x71, 0x1b, 0x31, 0xe3, 0x67, 0x49, 0xf6, 0x4c, - 0x27, 0x98, 0xd4, 0xd0, 0x14, 0x48, 0x1a, 0x45, 0x81, 0x84, 0xfe, 0x8d, 0x07, 0x5d, 0x94, 0x94, - 0x30, 0x1e, 0x1f, 0x24, 0x51, 0x38, 0x9c, 0x09, 0x89, 0xd1, 0x42, 0x31, 0x18, 0xb1, 0x88, 0x07, - 0x46, 0x62, 0x5c, 0x30, 0xda, 0xfc, 0x49, 0x18, 0x0b, 0x95, 0xa5, 0xe4, 0xc5, 0x8c, 0x51, 0xf2, - 0xd1, 0x76, 0x1d, 0x05, 0x39, 0x1b, 0x4c, 0x30, 0x9c, 0x50, 0xba, 0xda, 0x01, 0xa2, 0xfa, 0x40, - 0x40, 0x16, 0x70, 0x36, 0x98, 0x84, 0x51, 0x14, 0x4a, 0x5c, 0x29, 0xe1, 0x75, 0x53, 0x18, 0x66, - 0xb5, 0x95, 0x9a, 0x78, 0x38, 0x1a, 0xcb, 0x5c, 0xa9, 0x72, 0x44, 0xcc, 0xf3, 0xb3, 0x20, 0x7a, - 0xde, 0x71, 0x5d, 0x2c, 0x48, 0xf9, 0x5a, 0xe7, 0xaa, 0xd7, 0x7a, 0x05, 0x5a, 0x28, 0x5e, 0xb7, - 0x85, 0x8f, 0x24, 0x6b, 0xd7, 0x05, 0x40, 0xcf, 0xee, 0x8a, 0xd9, 0xf9, 0x62, 0x56, 0x00, 0x1c, - 0xaf, 0x68, 0xa1, 0xe4, 0x15, 0x7d, 0x00, 0x1d, 0x45, 0x46, 0xf0, 0x5d, 0x84, 0x4b, 0x85, 0x80, - 0x3b, 0x77, 0xe2, 0x3b, 0x98, 0xfa, 0xcb, 0x5d, 0xfd, 0xe5, 0xd2, 0xeb, 0xbe, 0xd4, 0x98, 0xf4, - 0x22, 0xac, 0x2b, 0xe6, 0x3d, 0xce, 0x82, 0xf4, 0x44, 0xab, 0xde, 0x91, 0x29, 0x90, 0x0a, 0x30, - 0xb9, 0x01, 0xf3, 0xf8, 0x99, 0xd6, 0x7e, 0xf5, 0x8f, 0x4e, 0xa2, 0x90, 0x6d, 0x98, 0x67, 0xa3, - 0x31, 0xd3, 0x9e, 0x39, 0x71, 0x63, 0x24, 0xbc, 0x23, 0x5f, 0x22, 0xa0, 0x0a, 0x40, 0x68, 0x49, - 0x05, 0xb8, 0x9a, 0x73, 0x01, 0x87, 0x1f, 0x8f, 0xe8, 0x06, 0x90, 0x27, 0x52, 0x6a, 0xed, 0xfc, - 0xde, 0xef, 0xcd, 0x41, 0xdb, 0x02, 0xe3, 0x6b, 0x1e, 0xe3, 0x86, 0x07, 0xa3, 0x30, 0x98, 0x30, - 0xce, 0x32, 0x25, 0xa9, 0x25, 0xa8, 0x50, 0xb0, 0xa7, 0xe3, 0x41, 0x32, 0xe5, 0x83, 0x11, 0x1b, - 0x67, 0x4c, 0x1a, 0x34, 0xcf, 0x2f, 0x41, 0x11, 0x6f, 0x12, 0x3c, 0xb7, 0xf1, 0xa4, 0x3c, 0x94, - 0xa0, 0x3a, 0x5b, 0x27, 0x79, 0xd4, 0x2c, 0xb2, 0x75, 0x92, 0x23, 0x65, 0x3d, 0x34, 0x5f, 0xa3, - 0x87, 0xde, 0x87, 0x4d, 0xa9, 0x71, 0xd4, 0xdb, 0x1c, 0x94, 0xc4, 0xe4, 0x9c, 0x59, 0x72, 0x03, - 0x56, 0x71, 0xcf, 0x5a, 0xc0, 0xf3, 0xf0, 0x7b, 0x32, 0xce, 0xf6, 0xfc, 0x0a, 0x1c, 0x71, 0xf1, - 0x39, 0x3a, 0xb8, 0xb2, 0x98, 0x50, 0x81, 0x0b, 0xdc, 0xe0, 0xb9, 0x8b, 0xdb, 0x52, 0xb8, 0x25, - 0x38, 0xed, 0x42, 0xfb, 0x90, 0x27, 0xa9, 0xbe, 0x94, 0x65, 0xe8, 0xc8, 0xa1, 0x2a, 0x20, 0x5d, - 0x86, 0x4b, 0x42, 0x8a, 0x9e, 0x26, 0x69, 0x12, 0x25, 0xe3, 0xd9, 0xe1, 0xf4, 0x28, 0x1f, 0x66, - 0x61, 0x8a, 0x1e, 0x33, 0xfd, 0x7b, 0x0f, 0xd6, 0x9d, 0x59, 0x15, 0xea, 0xff, 0xa2, 0x14, 0x69, - 0x93, 0xf3, 0x97, 0x82, 0xb7, 0x66, 0xa9, 0x43, 0x89, 0x28, 0x53, 0x22, 0x9f, 0xa9, 0x32, 0xc0, - 0x5d, 0x58, 0xd1, 0x3b, 0xd3, 0x1f, 0x4a, 0x29, 0xec, 0x55, 0xa5, 0x50, 0x7d, 0xbf, 0xac, 0x3e, - 0xd0, 0x24, 0x7e, 0x45, 0xfa, 0x9d, 0x6c, 0x24, 0xce, 0xa8, 0x63, 0xbe, 0xbe, 0xfe, 0xde, 0x76, - 0x76, 0xf5, 0x0e, 0x86, 0x06, 0x98, 0xd3, 0x3f, 0xf2, 0x00, 0x8a, 0xdd, 0xa1, 0x60, 0x14, 0x2a, - 0xdd, 0x13, 0xd9, 0x50, 0x4b, 0x7d, 0x5f, 0x87, 0x8e, 0xc9, 0x39, 0x17, 0x56, 0xa2, 0xad, 0x61, - 0xe8, 0xa1, 0xbc, 0x0b, 0x2b, 0xe3, 0x28, 0x39, 0x12, 0x36, 0x57, 0xd4, 0x2a, 0x73, 0x55, 0x46, - 0x5b, 0x96, 0xe0, 0x47, 0x0a, 0x5a, 0x98, 0x94, 0xa6, 0x65, 0x52, 0xe8, 0x1f, 0x37, 0x4c, 0xe6, - 0xb3, 0x38, 0xf3, 0xb9, 0xaf, 0x8c, 0xec, 0x56, 0x94, 0xe3, 0x39, 0x89, 0x46, 0x91, 0xdd, 0x38, - 0x78, 0x6d, 0xa0, 0x77, 0x07, 0x96, 0x33, 0xa9, 0x7d, 0xb4, 0x6a, 0x6a, 0xbe, 0x42, 0x35, 0x75, - 0x33, 0xc7, 0xee, 0xfc, 0x3f, 0x58, 0x0d, 0x46, 0xa7, 0x2c, 0xe3, 0xa1, 0xf0, 0xf8, 0x85, 0xd1, - 0x97, 0x0a, 0x75, 0xc5, 0x82, 0x0b, 0x5b, 0xfc, 0x2e, 0xac, 0xa8, 0xd2, 0xa5, 0xc1, 0x54, 0x1d, - 0x29, 0x05, 0x18, 0x11, 0xe9, 0x5f, 0xea, 0x24, 0xab, 0x7b, 0x87, 0xe7, 0x73, 0xc4, 0x3e, 0x5d, - 0xa3, 0x74, 0xba, 0xaf, 0xaa, 0x84, 0xe7, 0x48, 0x87, 0x15, 0x2a, 0xf5, 0x2c, 0x81, 0x2a, 0x41, - 0xed, 0xb2, 0xb4, 0xf9, 0x26, 0x2c, 0xa5, 0x3f, 0x9a, 0x83, 0xc5, 0x8f, 0xe3, 0xd3, 0x24, 0x1c, - 0x8a, 0xf4, 0xe3, 0x84, 0x4d, 0x12, 0xdd, 0x40, 0x80, 0xbf, 0xd1, 0xa2, 0x8b, 0xda, 0x58, 0xca, - 0x55, 0x5e, 0x50, 0x0f, 0xd1, 0xba, 0x65, 0x45, 0xd3, 0x8c, 0x94, 0x14, 0x0b, 0x82, 0xfe, 0x61, - 0x66, 0xf7, 0x01, 0xa9, 0x51, 0xd1, 0x81, 0x31, 0x6f, 0x75, 0x60, 0x88, 0x64, 0xb5, 0x2c, 0xfb, - 0x09, 0x76, 0x2e, 0xf9, 0x7a, 0x28, 0xfc, 0xd8, 0x8c, 0xc9, 0xa0, 0x57, 0xd8, 0xc9, 0x45, 0xe5, - 0xc7, 0xda, 0x40, 0xb4, 0xa5, 0xf2, 0x03, 0x89, 0x23, 0x75, 0x8d, 0x0d, 0x42, 0xdf, 0xa2, 0xdc, - 0x4a, 0xd4, 0x92, 0x57, 0x5c, 0x02, 0xa3, 0x42, 0x1a, 0x31, 0xa3, 0x37, 0xe4, 0x19, 0x40, 0x36, - 0x05, 0x95, 0xe1, 0x96, 0x17, 0x2c, 0xcb, 0x97, 0x6a, 0x24, 0x7c, 0x90, 0x20, 0x8a, 0x8e, 0x82, - 0xe1, 0x33, 0xd1, 0xe0, 0x25, 0xaa, 0x95, 0x2d, 0xdf, 0x05, 0xe2, 0xae, 0x87, 0x11, 0x3f, 0x1d, - 0x28, 0x12, 0x5d, 0x59, 0x6d, 0xb4, 0x40, 0xf4, 0x73, 0x20, 0x77, 0x47, 0x23, 0x75, 0x43, 0x26, - 0x46, 0x28, 0x78, 0xeb, 0x39, 0xbc, 0xad, 0x39, 0x63, 0xa3, 0xf6, 0x8c, 0xf4, 0x21, 0xb4, 0x0f, - 0xac, 0xbe, 0x2c, 0x71, 0x99, 0xba, 0x23, 0x4b, 0x09, 0x80, 0x05, 0xb1, 0x16, 0x6c, 0xd8, 0x0b, - 0xd2, 0x5f, 0x02, 0xb2, 0x1f, 0xe6, 0xdc, 0xec, 0xcf, 0x84, 0x8a, 0x26, 0xe3, 0x65, 0x85, 0x8a, - 0x0a, 0x26, 0x42, 0xc5, 0xbb, 0xb2, 0xe8, 0x59, 0x3e, 0xd8, 0x0d, 0x58, 0x0a, 0x25, 0x48, 0xeb, - 0xe1, 0x65, 0x25, 0xc0, 0x1a, 0xd3, 0xcc, 0xa3, 0x43, 0xa1, 0x80, 0x8e, 0x9a, 0xff, 0x81, 0x07, - 0x8b, 0xea, 0x68, 0x68, 0x0e, 0x9d, 0x8e, 0x34, 0x79, 0x30, 0x07, 0x56, 0xdf, 0x11, 0x54, 0x95, - 0xba, 0xb9, 0x3a, 0xa9, 0x23, 0xd0, 0x4c, 0x03, 0x7e, 0x22, 0x3c, 0xe8, 0x96, 0x2f, 0x7e, 0xeb, - 0x48, 0x69, 0xde, 0x44, 0x4a, 0xba, 0xc0, 0xab, 0x36, 0x65, 0xb2, 0x96, 0xf7, 0x64, 0x81, 0xb7, - 0x00, 0x17, 0x3c, 0x50, 0x1b, 0x2c, 0xf3, 0x40, 0xa1, 0xfa, 0x66, 0x9e, 0xf6, 0xa1, 0xf7, 0x80, - 0x45, 0x8c, 0xb3, 0xbb, 0x51, 0x54, 0xa6, 0x7f, 0x19, 0x2e, 0xd5, 0xcc, 0x29, 0x4b, 0xf9, 0x08, - 0xd6, 0x1e, 0xb0, 0xa3, 0xe9, 0x78, 0x9f, 0x9d, 0x16, 0x05, 0x03, 0x02, 0xcd, 0xfc, 0x24, 0x39, - 0x53, 0xf7, 0x25, 0x7e, 0x93, 0xb7, 0x00, 0x22, 0xc4, 0x19, 0xe4, 0x29, 0x1b, 0xea, 0x76, 0x16, - 0x01, 0x39, 0x4c, 0xd9, 0x90, 0xbe, 0x0f, 0xc4, 0xa6, 0xa3, 0x8e, 0x80, 0xaf, 0x71, 0x7a, 0x34, - 0xc8, 0x67, 0x39, 0x67, 0x13, 0xdd, 0xa7, 0x63, 0x83, 0xe8, 0xbb, 0xd0, 0x39, 0x08, 0x66, 0x3e, - 0xfb, 0x52, 0x35, 0xfa, 0x61, 0x40, 0x16, 0xcc, 0x50, 0x3c, 0x4d, 0x40, 0x26, 0xa6, 0xe9, 0xdf, - 0x35, 0x60, 0x41, 0x62, 0x22, 0xd5, 0x11, 0xcb, 0x79, 0x18, 0xcb, 0xb4, 0xba, 0xa2, 0x6a, 0x81, - 0x2a, 0xf7, 0xdd, 0xa8, 0xb9, 0x6f, 0xe5, 0x22, 0xe9, 0xd2, 0xbf, 0xba, 0x58, 0x07, 0x26, 0xe2, - 0xcd, 0x70, 0xc2, 0x64, 0xbf, 0x67, 0x53, 0xc5, 0x9b, 0x1a, 0x50, 0x8a, 0x7c, 0x8b, 0x37, 0x2f, - 0xf7, 0xa7, 0x05, 0x51, 0x99, 0x05, 0x1b, 0x54, 0xab, 0x59, 0x16, 0x65, 0x67, 0x5f, 0x45, 0xb3, - 0x54, 0x34, 0xc8, 0xd2, 0x1b, 0x68, 0x10, 0xe9, 0x37, 0x39, 0x1a, 0x84, 0xc0, 0xea, 0x23, 0xc6, - 0x7c, 0x96, 0x26, 0x99, 0xe9, 0x96, 0xfc, 0xa1, 0x07, 0xab, 0xca, 0x22, 0x98, 0x39, 0x72, 0xdd, - 0x31, 0x1f, 0x5e, 0x5d, 0xa6, 0xf5, 0x6d, 0xe8, 0x8a, 0x00, 0x0a, 0xa3, 0x23, 0x11, 0x2d, 0xa9, - 0x9c, 0x82, 0x03, 0xc4, 0x3d, 0xe9, 0xdc, 0xe1, 0x24, 0x8c, 0x14, 0x83, 0x6d, 0x10, 0x9a, 0x3a, - 0x1d, 0x60, 0x09, 0xf6, 0x7a, 0xbe, 0x19, 0xd3, 0x03, 0x58, 0xb3, 0xf6, 0xab, 0x04, 0xea, 0x0e, - 0xe8, 0x7a, 0xa3, 0x4c, 0x11, 0xc8, 0x77, 0xb1, 0xe5, 0x1a, 0xb7, 0xe2, 0x33, 0x07, 0x99, 0xfe, - 0xb3, 0x07, 0xeb, 0xd2, 0xd0, 0x2b, 0x37, 0xca, 0xb4, 0x28, 0x2d, 0x48, 0xcf, 0x46, 0x0a, 0xfc, - 0xde, 0x05, 0x5f, 0x8d, 0xc9, 0xd7, 0xdf, 0xd0, 0x39, 0x31, 0xa5, 0xbd, 0x73, 0xd8, 0x33, 0x57, - 0xc7, 0x9e, 0x57, 0x1c, 0xbe, 0x2e, 0x00, 0x9e, 0xaf, 0x0d, 0x80, 0xef, 0x2d, 0xc2, 0x7c, 0x3e, - 0x4c, 0x52, 0x46, 0x37, 0x61, 0xc3, 0x3d, 0x9c, 0x64, 0xd9, 0xee, 0xbf, 0x78, 0xb0, 0x2c, 0x93, - 0x71, 0xb2, 0x0f, 0x9b, 0x65, 0x04, 0x63, 0x2d, 0xab, 0xbd, 0x9b, 0x18, 0x57, 0xb3, 0xda, 0x26, - 0xde, 0xbf, 0x5c, 0x3b, 0xa7, 0xfd, 0xec, 0xef, 0xff, 0xe4, 0xdf, 0xfe, 0xa4, 0x71, 0x91, 0xae, - 0xee, 0x9c, 0xde, 0xde, 0x11, 0x2a, 0x91, 0x9d, 0x09, 0x8c, 0x0f, 0xbd, 0x1b, 0xb8, 0x8a, 0xdd, - 0xf9, 0x6d, 0x56, 0xa9, 0xe9, 0x20, 0x37, 0xab, 0xd4, 0xb6, 0x8a, 0x3b, 0xab, 0x4c, 0x05, 0x86, - 0x59, 0x65, 0xf7, 0xbf, 0xfa, 0xd0, 0x32, 0x41, 0x21, 0xf9, 0x2e, 0x74, 0x9d, 0xc4, 0x23, 0xd1, - 0x84, 0xeb, 0x52, 0x99, 0xfd, 0x2b, 0xf5, 0x93, 0x6a, 0xd9, 0xab, 0x62, 0xd9, 0x1e, 0xd9, 0xc4, - 0x65, 0x55, 0xb6, 0x6f, 0x47, 0x64, 0x64, 0x65, 0x8f, 0xc2, 0x33, 0x58, 0x76, 0x93, 0x85, 0xe4, - 0x8a, 0x2b, 0x1a, 0xa5, 0xd5, 0xde, 0x3a, 0x67, 0x56, 0x2d, 0x77, 0x45, 0x2c, 0xb7, 0x49, 0x36, - 0xec, 0xe5, 0x4c, 0xb0, 0xc6, 0x44, 0x57, 0x89, 0xdd, 0x12, 0x4e, 0x34, 0xbd, 0xfa, 0x56, 0xf1, - 0xfe, 0xa5, 0x6a, 0xfb, 0xb7, 0xea, 0x17, 0xa7, 0x3d, 0xb1, 0x14, 0x21, 0x82, 0xa1, 0x76, 0x47, - 0x38, 0xf9, 0x0e, 0xb4, 0x4c, 0x43, 0x29, 0xd9, 0xb2, 0xba, 0x78, 0xed, 0x2e, 0xd7, 0x7e, 0xaf, - 0x3a, 0x51, 0x77, 0x55, 0x36, 0x65, 0x14, 0x88, 0x7d, 0xb8, 0xa8, 0x2c, 0xf4, 0x11, 0xfb, 0x69, - 0x4e, 0x52, 0xd3, 0xc8, 0x7e, 0xcb, 0x23, 0x77, 0x60, 0x49, 0xf7, 0xe9, 0x92, 0xcd, 0xfa, 0x7e, - 0xe3, 0xfe, 0x56, 0x05, 0xae, 0xf4, 0xc8, 0x5d, 0x80, 0xa2, 0xa5, 0x94, 0xf4, 0xce, 0xeb, 0x7c, - 0x35, 0x4c, 0xac, 0xe9, 0x3f, 0x1d, 0x8b, 0x8e, 0x5a, 0xb7, 0x63, 0x95, 0x7c, 0xa5, 0xc0, 0xaf, - 0xed, 0x65, 0x7d, 0x05, 0x41, 0xba, 0x29, 0x78, 0xb7, 0x4a, 0x96, 0x91, 0x77, 0x31, 0x3b, 0xd3, - 0xfd, 0x55, 0x0f, 0xa0, 0x6d, 0xb5, 0xa9, 0x12, 0x4d, 0xa1, 0xda, 0xe2, 0xda, 0xef, 0xd7, 0x4d, - 0xa9, 0xed, 0xfe, 0x1a, 0x74, 0x9d, 0x7e, 0x53, 0xf3, 0x32, 0xea, 0xba, 0x59, 0xcd, 0xcb, 0xa8, - 0x6f, 0x51, 0xfd, 0x36, 0xb4, 0xad, 0xee, 0x50, 0x62, 0x55, 0xb4, 0x4b, 0xdd, 0x9f, 0x66, 0x47, - 0x35, 0xcd, 0xa4, 0x74, 0x43, 0x9c, 0x77, 0x99, 0xb6, 0xf0, 0xbc, 0xa2, 0xc9, 0x08, 0x85, 0xe4, - 0xbb, 0xb0, 0xec, 0x76, 0x85, 0x9a, 0x57, 0x55, 0xdb, 0x5f, 0x6a, 0x5e, 0xd5, 0x39, 0xad, 0xa4, - 0x4a, 0x20, 0x6f, 0xac, 0x9b, 0x45, 0x76, 0x5e, 0xa8, 0x94, 0xe8, 0x4b, 0xf2, 0x2d, 0x54, 0x1d, - 0xaa, 0xeb, 0x8b, 0x14, 0x5d, 0xb2, 0x6e, 0x6f, 0x98, 0x91, 0xf6, 0x4a, 0x83, 0x18, 0x5d, 0x13, - 0xc4, 0xdb, 0xa4, 0x38, 0x01, 0xf9, 0x04, 0x16, 0x55, 0xf7, 0x17, 0xb9, 0x58, 0x48, 0xb5, 0x95, - 0x40, 0xea, 0x6f, 0x96, 0xc1, 0x8a, 0xd8, 0xba, 0x20, 0xd6, 0x25, 0x6d, 0x24, 0x36, 0x66, 0x3c, - 0x44, 0x1a, 0x31, 0xac, 0x94, 0xaa, 0x58, 0xe6, 0xb1, 0xd4, 0xd7, 0xc0, 0xfb, 0x57, 0x5f, 0x5d, - 0xfc, 0x72, 0xd5, 0x8c, 0x56, 0x2f, 0x3b, 0xba, 0x65, 0xe1, 0x37, 0xa1, 0x63, 0x37, 0x1b, 0x1a, - 0x9d, 0x5d, 0xd3, 0x98, 0x68, 0x74, 0x76, 0x5d, 0x77, 0xa2, 0xbe, 0x5c, 0xd2, 0xb1, 0x97, 0x21, - 0xdf, 0x86, 0x15, 0xab, 0x5e, 0x7a, 0x38, 0x8b, 0x87, 0x46, 0x78, 0xaa, 0x7d, 0x2b, 0xfd, 0x3a, - 0x4b, 0x4b, 0xb7, 0x04, 0xe1, 0x35, 0xea, 0x10, 0x46, 0xc1, 0xb9, 0x0f, 0x6d, 0xbb, 0x16, 0xfb, - 0x0a, 0xba, 0x5b, 0xd6, 0x94, 0xdd, 0xec, 0x71, 0xcb, 0x23, 0x7f, 0xe6, 0x41, 0xc7, 0xee, 0x88, - 0x22, 0x4e, 0x16, 0xa6, 0x44, 0xa7, 0x67, 0xcf, 0xd9, 0x84, 0xe8, 0x13, 0xb1, 0xc9, 0xbd, 0x1b, - 0x8f, 0x1c, 0x26, 0xbf, 0x70, 0x9c, 0xa8, 0x9b, 0xf6, 0x7f, 0x38, 0x5e, 0x96, 0x27, 0xed, 0xbe, - 0x9e, 0x97, 0xb7, 0x3c, 0xf2, 0xa1, 0xfc, 0xa7, 0x8e, 0x0e, 0x68, 0x88, 0xa5, 0xd8, 0xca, 0xec, - 0xb2, 0xff, 0xfe, 0xb2, 0xed, 0xdd, 0xf2, 0xc8, 0x6f, 0xc9, 0xbf, 0x6d, 0xa8, 0x6f, 0x05, 0xd7, - 0xdf, 0xf4, 0x7b, 0xfa, 0xb6, 0x38, 0xc9, 0x55, 0x7a, 0xc9, 0x39, 0x49, 0x59, 0xb3, 0x1f, 0x00, - 0x14, 0xd1, 0x29, 0x29, 0x85, 0x6a, 0x46, 0xe7, 0x55, 0x03, 0x58, 0xf7, 0x36, 0x75, 0x44, 0x27, - 0xd5, 0x40, 0xc7, 0x8a, 0x0b, 0x73, 0x73, 0x9d, 0xd5, 0x28, 0xb3, 0xdf, 0xaf, 0x9b, 0x52, 0xf4, - 0xbf, 0x2a, 0xe8, 0xbf, 0x45, 0x2e, 0xdb, 0xf4, 0x77, 0x5e, 0xd8, 0x51, 0xe9, 0x4b, 0xf2, 0x39, - 0x74, 0xf7, 0x93, 0xe4, 0xd9, 0x34, 0x35, 0x09, 0x10, 0x37, 0xce, 0xc2, 0xc8, 0xb8, 0x5f, 0x3a, - 0x14, 0xbd, 0x2e, 0x28, 0x5f, 0x26, 0x97, 0x5c, 0xca, 0x45, 0xac, 0xfc, 0x92, 0x04, 0xb0, 0x66, - 0xec, 0x9d, 0x39, 0x48, 0xdf, 0xa5, 0x63, 0x87, 0xac, 0x95, 0x35, 0x1c, 0x0f, 0xc4, 0xac, 0x91, - 0x6b, 0x9a, 0xb7, 0x3c, 0x72, 0x00, 0x9d, 0x07, 0x6c, 0x98, 0x8c, 0x98, 0x0a, 0x8d, 0xd6, 0x8b, - 0x9d, 0x9b, 0x98, 0xaa, 0xdf, 0x75, 0x80, 0xae, 0x06, 0x48, 0x83, 0x59, 0xc6, 0xbe, 0xdc, 0x79, - 0xa1, 0x82, 0xae, 0x97, 0x5a, 0x03, 0xe8, 0x40, 0xd1, 0xd1, 0x00, 0xa5, 0xc8, 0xd2, 0xd1, 0x00, - 0x95, 0xc8, 0xd2, 0xd1, 0x00, 0x3a, 0x50, 0x25, 0x11, 0xc6, 0x9b, 0xa5, 0x60, 0xd4, 0x58, 0xcd, - 0xf3, 0x42, 0xd8, 0xfe, 0xb5, 0xf3, 0x11, 0xdc, 0xd5, 0x6e, 0xb8, 0xab, 0x1d, 0x42, 0xf7, 0x01, - 0x93, 0xcc, 0x92, 0x55, 0x85, 0xbe, 0xab, 0x52, 0xec, 0x0a, 0x44, 0x59, 0xdd, 0x88, 0x39, 0x57, - 0xc5, 0x8b, 0x94, 0x3e, 0xf9, 0x0e, 0xb4, 0x1f, 0x33, 0xae, 0xcb, 0x08, 0xc6, 0xf7, 0x28, 0xd5, - 0x15, 0xfa, 0x35, 0x55, 0x08, 0x7a, 0x4d, 0x50, 0xeb, 0x93, 0x9e, 0xa1, 0xb6, 0xc3, 0x46, 0x63, - 0x26, 0x1f, 0xff, 0x20, 0x1c, 0xbd, 0x24, 0xbf, 0x2e, 0x88, 0x9b, 0xca, 0xe3, 0xa6, 0x95, 0x7d, - 0xb6, 0x89, 0xaf, 0x94, 0xe0, 0x75, 0x94, 0xe3, 0x64, 0xc4, 0x2c, 0x63, 0x17, 0x43, 0xdb, 0x2a, - 0x33, 0x9b, 0x07, 0x55, 0xad, 0x5d, 0x9b, 0x07, 0x55, 0x53, 0x95, 0xa6, 0xdb, 0x62, 0x1d, 0x4a, - 0xae, 0x15, 0xeb, 0xc8, 0x4a, 0x74, 0xb1, 0xd2, 0xce, 0x8b, 0x60, 0xc2, 0x5f, 0x92, 0x2f, 0x44, - 0x6b, 0xb4, 0x5d, 0x2a, 0x29, 0x7c, 0x9f, 0x72, 0x55, 0xc5, 0x30, 0xcb, 0x9a, 0x72, 0xfd, 0x21, - 0xb9, 0x94, 0xb0, 0x89, 0x5f, 0x07, 0x38, 0xe4, 0x49, 0xfa, 0x20, 0x60, 0x93, 0x24, 0x2e, 0x34, - 0x59, 0x51, 0x0e, 0x28, 0x34, 0x99, 0x55, 0x13, 0x20, 0x5f, 0x58, 0xde, 0xa7, 0x53, 0x69, 0xd2, - 0xc2, 0x75, 0x6e, 0xc5, 0xc0, 0x30, 0xa4, 0xa6, 0x6a, 0x70, 0xcb, 0x43, 0x5f, 0xb2, 0x48, 0x7d, - 0x18, 0x5f, 0xb2, 0x92, 0x55, 0x31, 0x6a, 0xb0, 0x26, 0x4f, 0x72, 0x00, 0xad, 0x22, 0xfe, 0xd6, - 0xe6, 0xa9, 0x1c, 0xad, 0x1b, 0x7b, 0x53, 0x09, 0x8b, 0xe9, 0xaa, 0x60, 0x15, 0x90, 0x25, 0x64, - 0x95, 0xa8, 0x94, 0x0f, 0x61, 0x5d, 0x6e, 0xd0, 0x18, 0x4f, 0x91, 0xe0, 0xd6, 0x27, 0xa9, 0x09, - 0x83, 0xcd, 0x6b, 0xae, 0x8b, 0x22, 0xb5, 0x77, 0x42, 0xcd, 0x0a, 0x1f, 0x7a, 0x37, 0x8e, 0x16, - 0xc4, 0x1f, 0x8c, 0xbf, 0xf6, 0xdf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x8e, 0x28, 0xc4, 0x0c, 0x92, - 0x3c, 0x00, 0x00, + // 4877 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x3b, 0x4d, 0x8f, 0x1c, 0x49, + 0x56, 0xce, 0xea, 0xea, 0x8f, 0x7a, 0x55, 0xd5, 0x1f, 0xd1, 0xed, 0xee, 0x72, 0xd9, 0xeb, 0xb5, + 0x63, 0x47, 0x33, 0x8d, 0x19, 0xdc, 0x76, 0x2f, 0x3b, 0xcc, 0x8e, 0x81, 0x91, 0xbf, 0x7b, 0xd8, + 0x1e, 0x4f, 0x6f, 0xb6, 0x67, 0x06, 0x76, 0x85, 0x8a, 0xec, 0xaa, 0xe8, 0xea, 0x5c, 0x67, 0x65, + 0xe6, 0x64, 0x46, 0x75, 0xbb, 0xd6, 0x58, 0x82, 0x05, 0x21, 0x21, 0x81, 0xf6, 0x00, 0x02, 0xed, + 0x61, 0xb9, 0x70, 0x81, 0x03, 0xbf, 0x00, 0x89, 0x1f, 0xb0, 0x12, 0x02, 0x69, 0x4f, 0x08, 0x2e, + 0x08, 0x4e, 0x70, 0xe6, 0xc0, 0x0d, 0xbd, 0xf8, 0xca, 0x88, 0xcc, 0x6c, 0xdb, 0xcb, 0x2e, 0xdc, + 0x2a, 0x5e, 0xbc, 0x7c, 0x11, 0xf1, 0xe2, 0xc5, 0xfb, 0x2e, 0x68, 0x65, 0xe9, 0xf0, 0x66, 0x9a, + 0x25, 0x3c, 0x21, 0xf3, 0x51, 0x9c, 0xa5, 0xc3, 0xfe, 0x95, 0x71, 0x92, 0x8c, 0x23, 0xb6, 0x13, + 0xa4, 0xe1, 0x4e, 0x10, 0xc7, 0x09, 0x0f, 0x78, 0x98, 0xc4, 0xb9, 0x44, 0xa2, 0xb7, 0x61, 0xfd, + 0x7e, 0xc6, 0x02, 0xce, 0x3e, 0x0f, 0xa2, 0x88, 0x71, 0x9f, 0x7d, 0x31, 0x65, 0x39, 0x27, 0x7d, + 0x58, 0x4a, 0x83, 0x3c, 0x3f, 0x4b, 0xb2, 0x51, 0xcf, 0xbb, 0xe6, 0x6d, 0x77, 0x7c, 0x33, 0xa6, + 0x9b, 0xb0, 0xe1, 0x7e, 0x92, 0xa7, 0x49, 0x9c, 0x33, 0x24, 0xf5, 0x69, 0x1c, 0x25, 0xc3, 0x67, + 0x3f, 0x11, 0x29, 0xf7, 0x13, 0x45, 0xea, 0x07, 0x0d, 0x68, 0x3f, 0xcd, 0x82, 0x38, 0x0f, 0x86, + 0xb8, 0x59, 0xd2, 0x83, 0x45, 0xfe, 0x7c, 0x70, 0x12, 0xe4, 0x27, 0x82, 0x44, 0xcb, 0xd7, 0x43, + 0xb2, 0x09, 0x0b, 0xc1, 0x24, 0x99, 0xc6, 0xbc, 0xd7, 0xb8, 0xe6, 0x6d, 0xcf, 0xf9, 0x6a, 0x44, + 0xde, 0x85, 0xb5, 0x78, 0x3a, 0x19, 0x0c, 0x93, 0xf8, 0x38, 0xcc, 0x26, 0xf2, 0xc8, 0xbd, 0xb9, + 0x6b, 0xde, 0xf6, 0xbc, 0x5f, 0x9d, 0x20, 0x57, 0x01, 0x8e, 0x70, 0x1b, 0x72, 0x89, 0xa6, 0x58, + 0xc2, 0x82, 0x10, 0x0a, 0x1d, 0x35, 0x62, 0xe1, 0xf8, 0x84, 0xf7, 0xe6, 0x05, 0x21, 0x07, 0x86, + 0x34, 0x78, 0x38, 0x61, 0x83, 0x9c, 0x07, 0x93, 0xb4, 0xb7, 0x20, 0x76, 0x63, 0x41, 0xc4, 0x7c, + 0xc2, 0x83, 0x68, 0x70, 0xcc, 0x58, 0xde, 0x5b, 0x54, 0xf3, 0x06, 0x42, 0xde, 0x86, 0xe5, 0x11, + 0xcb, 0xf9, 0x20, 0x18, 0x8d, 0x32, 0x96, 0xe7, 0x2c, 0xef, 0x2d, 0x5d, 0x9b, 0xdb, 0x6e, 0xf9, + 0x25, 0x28, 0xed, 0xc1, 0xe6, 0x63, 0xc6, 0x2d, 0xee, 0xe4, 0x8a, 0xd3, 0x74, 0x1f, 0x88, 0x05, + 0x7e, 0xc0, 0x78, 0x10, 0x46, 0x39, 0x79, 0x0f, 0x3a, 0xdc, 0x42, 0xee, 0x79, 0xd7, 0xe6, 0xb6, + 0xdb, 0xbb, 0xe4, 0xa6, 0x90, 0x8e, 0x9b, 0xd6, 0x07, 0xbe, 0x83, 0x47, 0xff, 0xd1, 0x83, 0xf6, + 0x21, 0x8b, 0x47, 0xfa, 0x1e, 0x09, 0x34, 0x71, 0x27, 0xea, 0x0e, 0xc5, 0x6f, 0xf2, 0x65, 0x68, + 0x8b, 0xdd, 0xe5, 0x3c, 0x0b, 0xe3, 0xb1, 0xb8, 0x82, 0x96, 0x0f, 0x08, 0x3a, 0x14, 0x10, 0xb2, + 0x0a, 0x73, 0xc1, 0x84, 0x0b, 0xc6, 0xcf, 0xf9, 0xf8, 0x93, 0x5c, 0x87, 0x4e, 0x1a, 0xcc, 0x26, + 0x2c, 0xe6, 0x05, 0xb3, 0x3b, 0x7e, 0x5b, 0xc1, 0xf6, 0x90, 0xdb, 0x37, 0x61, 0xdd, 0x46, 0xd1, + 0xd4, 0xe7, 0x05, 0xf5, 0x35, 0x0b, 0x53, 0x2d, 0xf2, 0x0e, 0xac, 0x68, 0xfc, 0x4c, 0x6e, 0x56, + 0xb0, 0xbf, 0xe5, 0x2f, 0x2b, 0xb0, 0x66, 0xd0, 0x9f, 0x7a, 0xd0, 0x91, 0x47, 0x92, 0x72, 0x46, + 0xde, 0x82, 0xae, 0xfe, 0x92, 0x65, 0x59, 0x92, 0x29, 0xe9, 0x72, 0x81, 0xe4, 0x06, 0xac, 0x6a, + 0x40, 0x9a, 0xb1, 0x70, 0x12, 0x8c, 0x99, 0x38, 0x6a, 0xc7, 0xaf, 0xc0, 0xc9, 0x6e, 0x41, 0x31, + 0x4b, 0xa6, 0x9c, 0x89, 0xa3, 0xb7, 0x77, 0x3b, 0x8a, 0xdd, 0x3e, 0xc2, 0x7c, 0x17, 0x85, 0x7e, + 0xcf, 0x83, 0xce, 0xfd, 0x93, 0x20, 0x8e, 0x59, 0x74, 0x90, 0x84, 0x31, 0x47, 0x71, 0x3b, 0x9e, + 0xc6, 0xa3, 0x30, 0x1e, 0x0f, 0xf8, 0xf3, 0x50, 0x3f, 0x1b, 0x07, 0x86, 0x9b, 0xb2, 0xc7, 0xc8, + 0x24, 0xc5, 0xff, 0x0a, 0x1c, 0xe9, 0x25, 0x53, 0x9e, 0x4e, 0xf9, 0x20, 0x8c, 0x47, 0xec, 0xb9, + 0xd8, 0x53, 0xd7, 0x77, 0x60, 0xf4, 0x57, 0x61, 0x75, 0x1f, 0xe5, 0x38, 0x0e, 0xe3, 0xf1, 0x5d, + 0x29, 0x6c, 0xf8, 0xb8, 0xd2, 0xe9, 0xd1, 0x33, 0x36, 0x53, 0x7c, 0x51, 0x23, 0x14, 0x85, 0x93, + 0x24, 0xe7, 0x6a, 0x3d, 0xf1, 0x9b, 0xfe, 0x9b, 0x07, 0x2b, 0xc8, 0xdb, 0x8f, 0x83, 0x78, 0xa6, + 0x45, 0x66, 0x1f, 0x3a, 0x48, 0xea, 0x69, 0x72, 0x57, 0x3e, 0x51, 0x29, 0x7a, 0xdb, 0x8a, 0x17, + 0x25, 0xec, 0x9b, 0x36, 0xea, 0xc3, 0x98, 0x67, 0x33, 0xdf, 0xf9, 0x1a, 0x85, 0x8d, 0x07, 0xd9, + 0x98, 0x71, 0xf1, 0x78, 0xd5, 0x63, 0x06, 0x09, 0xba, 0x9f, 0xc4, 0xc7, 0xe4, 0x1a, 0x74, 0xf2, + 0x80, 0x0f, 0x52, 0x96, 0x0d, 0x8e, 0x66, 0x9c, 0x09, 0x81, 0x99, 0xf3, 0x21, 0x0f, 0xf8, 0x01, + 0xcb, 0xee, 0xcd, 0x38, 0xeb, 0x7f, 0x08, 0x6b, 0x95, 0x55, 0x50, 0x46, 0x8b, 0x23, 0xe2, 0x4f, + 0xb2, 0x01, 0xf3, 0xa7, 0x41, 0x34, 0x65, 0x4a, 0xa7, 0xc8, 0xc1, 0x07, 0x8d, 0xf7, 0x3d, 0xfa, + 0x36, 0xac, 0x16, 0xdb, 0x56, 0x42, 0x44, 0xa0, 0x69, 0x6e, 0xa9, 0xe5, 0x8b, 0xdf, 0xf4, 0x77, + 0x3d, 0x89, 0x78, 0x3f, 0x09, 0xcd, 0xfb, 0x44, 0x44, 0x7c, 0xc6, 0x1a, 0x11, 0x7f, 0x9f, 0xab, + 0xbf, 0x7e, 0xfa, 0xc3, 0xd2, 0x77, 0x60, 0xcd, 0xda, 0xc2, 0x2b, 0x36, 0xfb, 0x17, 0x1e, 0xac, + 0x3d, 0x61, 0x67, 0xea, 0xd6, 0xf5, 0x6e, 0xdf, 0x87, 0x26, 0x9f, 0xa5, 0x4c, 0x60, 0x2e, 0xef, + 0xbe, 0xa5, 0x2e, 0xad, 0x82, 0x77, 0x53, 0x0d, 0x9f, 0xce, 0x52, 0xe6, 0x8b, 0x2f, 0xe8, 0x27, + 0xd0, 0xb6, 0x80, 0x64, 0x0b, 0xd6, 0x3f, 0xff, 0xe8, 0xe9, 0x93, 0x87, 0x87, 0x87, 0x83, 0x83, + 0x4f, 0xef, 0x7d, 0xe3, 0xe1, 0x6f, 0x0c, 0xf6, 0xee, 0x1e, 0xee, 0xad, 0x5e, 0x20, 0x9b, 0x40, + 0x9e, 0x3c, 0x3c, 0x7c, 0xfa, 0xf0, 0x81, 0x03, 0xf7, 0xc8, 0x0a, 0xb4, 0x6d, 0x40, 0x83, 0xf6, + 0xa1, 0xf7, 0x84, 0x9d, 0x7d, 0x1e, 0xf2, 0x98, 0xe5, 0xb9, 0xbb, 0x3c, 0xbd, 0x09, 0xc4, 0xde, + 0x93, 0x3a, 0x66, 0x0f, 0x16, 0x95, 0xc6, 0xd4, 0x06, 0x43, 0x0d, 0xe9, 0xdb, 0x40, 0x0e, 0xc3, + 0x71, 0xfc, 0x31, 0xcb, 0xf3, 0x60, 0xcc, 0xf4, 0x61, 0x57, 0x61, 0x6e, 0x92, 0x8f, 0xd5, 0x43, + 0xc3, 0x9f, 0xf4, 0xab, 0xb0, 0xee, 0xe0, 0x29, 0xc2, 0x57, 0xa0, 0x95, 0x87, 0xe3, 0x38, 0xe0, + 0xd3, 0x8c, 0x29, 0xd2, 0x05, 0x80, 0x3e, 0x82, 0x8d, 0xcf, 0x58, 0x16, 0x1e, 0xcf, 0x5e, 0x47, + 0xde, 0xa5, 0xd3, 0x28, 0xd3, 0x79, 0x08, 0x17, 0x4b, 0x74, 0xd4, 0xf2, 0x52, 0x32, 0xd5, 0xfd, + 0x2d, 0xf9, 0x72, 0x60, 0xbd, 0xd3, 0x86, 0xfd, 0x4e, 0xe9, 0xa7, 0x40, 0xee, 0x27, 0x71, 0xcc, + 0x86, 0xfc, 0x80, 0xb1, 0x4c, 0x6f, 0xe6, 0xe7, 0x2d, 0x31, 0x6c, 0xef, 0x6e, 0xa9, 0x8b, 0x2d, + 0x3f, 0x7e, 0x25, 0x9f, 0x04, 0x9a, 0x29, 0xcb, 0x26, 0x82, 0xf0, 0x92, 0x2f, 0x7e, 0xd3, 0x1d, + 0x58, 0x77, 0xc8, 0x16, 0x3c, 0x4f, 0x19, 0xcb, 0x06, 0x6a, 0x77, 0xf3, 0xbe, 0x1e, 0xd2, 0xdb, + 0x70, 0xf1, 0x41, 0x98, 0x0f, 0xab, 0x5b, 0xc1, 0x4f, 0xa6, 0x47, 0x83, 0xe2, 0xf9, 0xe9, 0x21, + 0x5a, 0xb9, 0xf2, 0x27, 0xca, 0x37, 0xf8, 0x03, 0x0f, 0x9a, 0x7b, 0x4f, 0xf7, 0xef, 0xa3, 0x63, + 0x11, 0xc6, 0xc3, 0x64, 0x82, 0xb6, 0x41, 0xb2, 0xc3, 0x8c, 0xcf, 0x7d, 0x56, 0x57, 0xa0, 0x25, + 0x4c, 0x0a, 0x1a, 0x6e, 0xf1, 0xa8, 0x3a, 0x7e, 0x01, 0x40, 0xa7, 0x81, 0x3d, 0x4f, 0xc3, 0x4c, + 0x78, 0x05, 0xda, 0xd6, 0x37, 0x85, 0xb2, 0xac, 0x4e, 0xd0, 0xff, 0x68, 0x42, 0xf7, 0xee, 0x90, + 0x87, 0xa7, 0x4c, 0x29, 0x6f, 0xb1, 0xaa, 0x00, 0xa8, 0xfd, 0xa8, 0x11, 0x9a, 0x99, 0x8c, 0x4d, + 0x12, 0xce, 0x06, 0xce, 0x35, 0xb9, 0x40, 0xc4, 0x1a, 0x4a, 0x42, 0x83, 0x14, 0xcd, 0x80, 0xd8, + 0x5f, 0xcb, 0x77, 0x81, 0xc8, 0x32, 0x04, 0x20, 0x97, 0x71, 0x67, 0x4d, 0x5f, 0x0f, 0x91, 0x1f, + 0xc3, 0x20, 0x0d, 0x86, 0x21, 0x9f, 0x29, 0x6d, 0x60, 0xc6, 0x48, 0x3b, 0x4a, 0x86, 0x41, 0x34, + 0x38, 0x0a, 0xa2, 0x20, 0x1e, 0x32, 0xe5, 0x9f, 0xb8, 0x40, 0x74, 0x41, 0xd4, 0x96, 0x34, 0x9a, + 0x74, 0x53, 0x4a, 0x50, 0x74, 0x65, 0x86, 0xc9, 0x64, 0x12, 0x72, 0xf4, 0x5c, 0x7a, 0x4b, 0x52, + 0xf3, 0x14, 0x10, 0x71, 0x12, 0x39, 0x3a, 0x93, 0x3c, 0x6c, 0xc9, 0xd5, 0x1c, 0x20, 0x52, 0x39, + 0x66, 0x4c, 0x68, 0xb0, 0x67, 0x67, 0x3d, 0x90, 0x54, 0x0a, 0x08, 0xde, 0xc6, 0x34, 0xce, 0x19, + 0xe7, 0x11, 0x1b, 0x99, 0x0d, 0xb5, 0x05, 0x5a, 0x75, 0x82, 0xdc, 0x82, 0x75, 0xe9, 0x4c, 0xe5, + 0x01, 0x4f, 0xf2, 0x93, 0x30, 0x1f, 0xe4, 0x2c, 0xe6, 0xbd, 0x8e, 0xc0, 0xaf, 0x9b, 0x22, 0xef, + 0xc3, 0x56, 0x09, 0x9c, 0xb1, 0x21, 0x0b, 0x4f, 0xd9, 0xa8, 0xd7, 0x15, 0x5f, 0x9d, 0x37, 0x4d, + 0xae, 0x41, 0x1b, 0x7d, 0xc8, 0x69, 0x3a, 0x0a, 0x38, 0xcb, 0x7b, 0xcb, 0xe2, 0x1e, 0x6c, 0x10, + 0xb9, 0x0d, 0xdd, 0x94, 0x49, 0x2b, 0x7c, 0xc2, 0xa3, 0x61, 0xde, 0x5b, 0x11, 0xa6, 0xaf, 0xad, + 0x1e, 0x1b, 0xca, 0xaf, 0xef, 0x62, 0xa0, 0x68, 0x0e, 0xf3, 0xd3, 0xc1, 0x88, 0x45, 0xc1, 0xac, + 0xb7, 0x2a, 0x84, 0xae, 0x00, 0xd0, 0x8b, 0xb0, 0xbe, 0x1f, 0xe6, 0x5c, 0x49, 0x9a, 0xd1, 0x7e, + 0x7b, 0xb0, 0xe1, 0x82, 0xd5, 0x5b, 0xbc, 0x05, 0x4b, 0x4a, 0x6c, 0xf2, 0x5e, 0x5b, 0x2c, 0xbd, + 0xa1, 0x96, 0x76, 0x24, 0xd6, 0x37, 0x58, 0xf4, 0xf7, 0x1b, 0xd0, 0xc4, 0x77, 0x76, 0xfe, 0x9b, + 0xb4, 0x1f, 0x78, 0xc3, 0x79, 0xe0, 0xb6, 0xba, 0x9d, 0x73, 0xd4, 0xad, 0xf0, 0xac, 0x67, 0x9c, + 0xa9, 0xdb, 0x90, 0x12, 0x6b, 0x41, 0x8a, 0xf9, 0x8c, 0x0d, 0x4f, 0x85, 0xd8, 0x9a, 0x79, 0x84, + 0xa0, 0x50, 0xa3, 0x99, 0x13, 0x5f, 0x4b, 0x99, 0x35, 0x63, 0x3d, 0x27, 0xbe, 0x5c, 0x2c, 0xe6, + 0xc4, 0x77, 0x3d, 0x58, 0x0c, 0xe3, 0xa3, 0x64, 0x1a, 0x8f, 0x84, 0x7c, 0x2e, 0xf9, 0x7a, 0x88, + 0x7c, 0x4e, 0x85, 0x77, 0x14, 0x4e, 0x98, 0x12, 0xcc, 0x02, 0x40, 0x09, 0xba, 0x41, 0xb9, 0xd0, + 0x38, 0x86, 0xc9, 0xef, 0xc1, 0x9a, 0x05, 0x53, 0x1c, 0xbe, 0x0e, 0xf3, 0x78, 0x7a, 0xed, 0x4f, + 0xeb, 0x9b, 0x15, 0xaa, 0x4a, 0xce, 0xd0, 0x55, 0x58, 0x7e, 0xcc, 0xf8, 0x47, 0xf1, 0x71, 0xa2, + 0x29, 0xfd, 0xe1, 0x1c, 0xac, 0x18, 0x90, 0x22, 0xb4, 0x0d, 0x2b, 0xe1, 0x88, 0xc5, 0x3c, 0xe4, + 0xb3, 0x81, 0xe3, 0x6d, 0x95, 0xc1, 0xa8, 0xfc, 0x83, 0x28, 0x0c, 0x72, 0xa5, 0x3e, 0xe4, 0x80, + 0xec, 0xc2, 0x06, 0x4a, 0x9e, 0x16, 0x26, 0x73, 0xed, 0xd2, 0xc9, 0xab, 0x9d, 0xc3, 0xc7, 0x82, + 0x70, 0xa9, 0x9e, 0x8a, 0x4f, 0xa4, 0xaa, 0xab, 0x9b, 0x42, 0xae, 0x49, 0x4a, 0x78, 0xe4, 0x79, + 0x29, 0x9d, 0x06, 0x50, 0x89, 0x8f, 0x16, 0xa4, 0x83, 0x59, 0x8e, 0x8f, 0xac, 0x18, 0x6b, 0xa9, + 0x12, 0x63, 0x6d, 0xc3, 0x4a, 0x3e, 0x8b, 0x87, 0x6c, 0x34, 0xe0, 0x09, 0xae, 0x1b, 0xc6, 0xe2, + 0x76, 0x96, 0xfc, 0x32, 0x58, 0x44, 0x83, 0x2c, 0xe7, 0x31, 0xe3, 0x42, 0x6b, 0x2c, 0xf9, 0x7a, + 0x88, 0x0a, 0x58, 0xa0, 0x48, 0xa1, 0x6f, 0xf9, 0x6a, 0x84, 0x56, 0x6c, 0x9a, 0x85, 0x79, 0xaf, + 0x23, 0xa0, 0xe2, 0x37, 0xfd, 0xae, 0x30, 0x8e, 0x26, 0x08, 0xfc, 0x54, 0xbc, 0x5c, 0x72, 0x19, + 0x5a, 0x72, 0x4f, 0xf9, 0x49, 0xa0, 0xc3, 0x55, 0x01, 0x38, 0x3c, 0x09, 0x30, 0x76, 0x71, 0x8e, + 0x29, 0x5f, 0x41, 0x5b, 0xc0, 0xf6, 0xe4, 0x29, 0xdf, 0x82, 0x65, 0x1d, 0x5e, 0xe6, 0x83, 0x88, + 0x1d, 0x73, 0xed, 0x6c, 0xc7, 0xd3, 0x09, 0x2e, 0x97, 0xef, 0xb3, 0x63, 0x4e, 0x9f, 0xc0, 0x9a, + 0x7a, 0x81, 0x9f, 0xa4, 0x4c, 0x2f, 0xfd, 0xf5, 0xb2, 0xfe, 0x97, 0x06, 0x7a, 0x5d, 0x49, 0x96, + 0x1d, 0x21, 0x94, 0x8c, 0x02, 0xf5, 0x81, 0xa8, 0xe9, 0xfb, 0x51, 0x92, 0x33, 0x45, 0x90, 0x42, + 0x67, 0x18, 0x25, 0x79, 0x39, 0x8c, 0xb0, 0x61, 0xc8, 0xcb, 0x7c, 0x3a, 0x1c, 0xe2, 0xcb, 0x95, + 0x26, 0x5e, 0x0f, 0xe9, 0x5f, 0x79, 0xb0, 0x2e, 0xa8, 0x69, 0x5d, 0x61, 0xfc, 0xc2, 0x37, 0xdf, + 0x66, 0x67, 0x68, 0x87, 0x35, 0x1b, 0x30, 0x7f, 0x9c, 0x64, 0x43, 0xa6, 0x56, 0x92, 0x83, 0x9f, + 0x85, 0xa7, 0xfb, 0x4f, 0x1e, 0xac, 0x89, 0xad, 0x1e, 0xf2, 0x80, 0x4f, 0x73, 0x75, 0xfc, 0x5f, + 0x86, 0x2e, 0x1e, 0x95, 0x69, 0xf1, 0x57, 0x1b, 0xdd, 0x30, 0x2f, 0x55, 0x40, 0x25, 0xf2, 0xde, + 0x05, 0xdf, 0x45, 0x26, 0x1f, 0x42, 0xc7, 0xce, 0x11, 0x88, 0x3d, 0xb7, 0x77, 0x2f, 0xe9, 0x53, + 0x56, 0x24, 0x67, 0xef, 0x82, 0xef, 0x7c, 0x40, 0xee, 0x00, 0x08, 0xcb, 0x2c, 0xc8, 0xaa, 0x30, + 0xf0, 0x92, 0xcb, 0x24, 0xeb, 0xb2, 0xf6, 0x2e, 0xf8, 0x16, 0xfa, 0xbd, 0x25, 0x58, 0x90, 0xa6, + 0x84, 0x3e, 0x86, 0xae, 0xb3, 0x53, 0xc7, 0x83, 0xef, 0x48, 0x0f, 0xbe, 0x12, 0xe0, 0x35, 0x6a, + 0x02, 0xbc, 0x7f, 0x6d, 0x00, 0x41, 0x69, 0x2b, 0x5d, 0xe7, 0xdb, 0xb0, 0xac, 0xd8, 0xef, 0x3a, + 0x6f, 0x25, 0xa8, 0xb0, 0x79, 0xc9, 0xc8, 0xf1, 0x60, 0x3a, 0xbe, 0x0d, 0x22, 0x37, 0x81, 0x58, + 0x43, 0x1d, 0xb5, 0x4b, 0x7b, 0x50, 0x33, 0x83, 0x8a, 0x4b, 0xba, 0x1f, 0x3a, 0x5e, 0x55, 0x1e, + 0x5b, 0x53, 0xdc, 0x6f, 0xed, 0x9c, 0x48, 0x26, 0x4d, 0xf3, 0x13, 0xb4, 0xc9, 0xda, 0xc7, 0xd1, + 0xe3, 0xb2, 0x20, 0x2d, 0xbc, 0x56, 0x90, 0x16, 0xcb, 0x82, 0x24, 0x2c, 0x5c, 0x16, 0x9e, 0x06, + 0x9c, 0x69, 0xab, 0xa1, 0x86, 0xe8, 0xd2, 0x4c, 0xc2, 0x58, 0x98, 0xea, 0xc1, 0x04, 0x57, 0x57, + 0x2e, 0x8d, 0x03, 0xa4, 0x3f, 0xf6, 0x60, 0x15, 0x79, 0xec, 0xc8, 0xe1, 0x07, 0x20, 0x9e, 0xc1, + 0x1b, 0x8a, 0xa1, 0x83, 0xfb, 0xd3, 0x4b, 0xe1, 0xfb, 0xd0, 0x12, 0x04, 0x93, 0x94, 0xc5, 0x4a, + 0x08, 0x7b, 0xae, 0x10, 0x16, 0x1a, 0x68, 0xef, 0x82, 0x5f, 0x20, 0x5b, 0x22, 0xf8, 0x0f, 0x1e, + 0xb4, 0xd5, 0x36, 0xff, 0xd7, 0x8e, 0x77, 0x1f, 0x96, 0x50, 0x1a, 0x2d, 0xbf, 0xd6, 0x8c, 0x51, + 0xf3, 0x4f, 0x30, 0xee, 0x41, 0x53, 0xe7, 0x38, 0xdd, 0x65, 0x30, 0xda, 0x2d, 0xa1, 0x6c, 0xf3, + 0x01, 0x0f, 0xa3, 0x81, 0x9e, 0x55, 0xe9, 0xb8, 0xba, 0x29, 0xd4, 0x39, 0x39, 0x0f, 0xc6, 0x4c, + 0x99, 0x24, 0x39, 0xc0, 0xe8, 0x42, 0x1d, 0xa8, 0xec, 0x50, 0xfd, 0x08, 0x60, 0xab, 0x32, 0x65, + 0x9c, 0x2a, 0xe5, 0x47, 0x46, 0xe1, 0xe4, 0x28, 0x31, 0x2e, 0xa9, 0x67, 0xbb, 0x98, 0xce, 0x14, + 0x19, 0xc3, 0x45, 0x6d, 0x7b, 0x91, 0xa7, 0x85, 0xa5, 0x6d, 0x08, 0xa7, 0xe1, 0xb6, 0x2b, 0x03, + 0xe5, 0x05, 0x35, 0xdc, 0x7e, 0xb5, 0xf5, 0xf4, 0xc8, 0x09, 0xf4, 0x8c, 0x91, 0x57, 0xea, 0xdd, + 0x72, 0x04, 0x70, 0xad, 0x77, 0x5f, 0xb3, 0x96, 0xd0, 0x45, 0x23, 0xbd, 0xcc, 0xb9, 0xd4, 0xc8, + 0x0c, 0xae, 0xea, 0x39, 0xa1, 0xbf, 0xab, 0xeb, 0x35, 0xdf, 0xe8, 0x6c, 0x8f, 0xf0, 0x63, 0x77, + 0xd1, 0xd7, 0x10, 0xee, 0xff, 0xc8, 0x83, 0x65, 0x97, 0x1c, 0x8a, 0x8e, 0x8a, 0x4d, 0xb4, 0x82, + 0xd1, 0xce, 0x53, 0x09, 0x5c, 0x8d, 0xae, 0x1a, 0x75, 0xd1, 0x95, 0x1d, 0x43, 0xcd, 0xbd, 0x2e, + 0x86, 0x6a, 0xbe, 0x59, 0x0c, 0x35, 0x5f, 0x17, 0x43, 0xf5, 0xff, 0xcb, 0x03, 0x52, 0xbd, 0x5f, + 0xf2, 0x58, 0x86, 0x77, 0x31, 0x8b, 0x94, 0x9e, 0xf8, 0x85, 0x37, 0x93, 0x11, 0xcd, 0x43, 0xfd, + 0x35, 0x0a, 0xab, 0xad, 0x08, 0x6c, 0x97, 0xa5, 0xeb, 0xd7, 0x4d, 0x95, 0xa2, 0xba, 0xe6, 0xeb, + 0xa3, 0xba, 0xf9, 0xd7, 0x47, 0x75, 0x0b, 0xe5, 0xa8, 0xae, 0xff, 0xdb, 0xd0, 0x75, 0x6e, 0xfd, + 0x67, 0x77, 0xe2, 0xb2, 0xbb, 0x23, 0x2f, 0xd8, 0x81, 0xf5, 0xff, 0xb3, 0x01, 0xa4, 0x2a, 0x79, + 0xff, 0xaf, 0x7b, 0x10, 0x72, 0xe4, 0x28, 0x90, 0x39, 0x25, 0x47, 0x8e, 0xea, 0xf8, 0xbf, 0x54, + 0x8a, 0xef, 0xc2, 0x5a, 0xc6, 0x86, 0xc9, 0x29, 0xcb, 0xac, 0xc8, 0x5a, 0x5e, 0x55, 0x75, 0x02, + 0x1d, 0x3e, 0x37, 0x96, 0x5d, 0x72, 0x2a, 0x08, 0x96, 0x65, 0x28, 0x85, 0xb4, 0xf4, 0xeb, 0xb0, + 0x21, 0x0b, 0x3b, 0xf7, 0x24, 0x29, 0xed, 0x73, 0x5c, 0x87, 0xce, 0x99, 0x4c, 0xe6, 0x0d, 0x92, + 0x38, 0x9a, 0x29, 0x23, 0xd2, 0x56, 0xb0, 0x4f, 0xe2, 0x68, 0x46, 0x7f, 0xe8, 0xc1, 0xc5, 0xd2, + 0xb7, 0x45, 0xce, 0x5e, 0xaa, 0x5a, 0x57, 0xff, 0xba, 0x40, 0x3c, 0xa2, 0x92, 0x71, 0xeb, 0x88, + 0xd2, 0x24, 0x55, 0x27, 0x90, 0x85, 0xd3, 0xb8, 0x8a, 0x2f, 0x2f, 0xa6, 0x6e, 0x8a, 0x6e, 0xc1, + 0x45, 0x75, 0xf9, 0xee, 0xd9, 0xe8, 0x2e, 0x6c, 0x96, 0x27, 0x8a, 0xfc, 0x98, 0xbb, 0x65, 0x3d, + 0xa4, 0x1f, 0x02, 0xf9, 0xe6, 0x94, 0x65, 0x33, 0x51, 0x1d, 0x30, 0x09, 0xd8, 0xad, 0x72, 0x20, + 0xbe, 0x90, 0x4e, 0x8f, 0xbe, 0xc1, 0x66, 0xba, 0xa8, 0xd2, 0x30, 0x45, 0x15, 0x7a, 0x07, 0xd6, + 0x1d, 0x02, 0x86, 0x55, 0x0b, 0xa2, 0xc2, 0xa0, 0x83, 0x54, 0xb7, 0x0a, 0xa1, 0xe6, 0xe8, 0x9f, + 0x7b, 0x30, 0xb7, 0x97, 0xa4, 0x76, 0x66, 0xc9, 0x73, 0x33, 0x4b, 0x4a, 0x77, 0x0e, 0x8c, 0x6a, + 0x6c, 0xa8, 0x97, 0x6f, 0x03, 0x51, 0xf3, 0x05, 0x13, 0x8e, 0x61, 0xda, 0x71, 0x92, 0x9d, 0x05, + 0xd9, 0x48, 0xf1, 0xaf, 0x04, 0xc5, 0xed, 0x17, 0x0a, 0x06, 0x7f, 0xa2, 0xd3, 0x20, 0xd2, 0x6b, + 0x33, 0x15, 0x59, 0xaa, 0x11, 0xfd, 0xbe, 0x07, 0xf3, 0x62, 0xaf, 0xf8, 0x1a, 0xe4, 0xfd, 0x8a, + 0x82, 0x9a, 0xc8, 0xde, 0x79, 0xf2, 0x35, 0x94, 0xc0, 0xa5, 0x32, 0x5b, 0xa3, 0x52, 0x66, 0xbb, + 0x02, 0x2d, 0x39, 0x2a, 0xea, 0x52, 0x05, 0x80, 0x5c, 0x85, 0xe6, 0x49, 0x92, 0x6a, 0x1b, 0x06, + 0x3a, 0x5d, 0x93, 0xa4, 0xbe, 0x80, 0xd3, 0x1b, 0xb0, 0xf2, 0x24, 0x19, 0x31, 0x2b, 0xa6, 0x3f, + 0xf7, 0x9a, 0xe8, 0xef, 0x78, 0xb0, 0xa4, 0x91, 0xc9, 0x36, 0x34, 0xd1, 0x14, 0x95, 0x9c, 0x3f, + 0x93, 0x74, 0x45, 0x3c, 0x5f, 0x60, 0xa0, 0x0a, 0x11, 0x11, 0x64, 0xe1, 0x2a, 0xe8, 0xf8, 0xb1, + 0x30, 0xc2, 0xe8, 0xb4, 0x8b, 0x3d, 0x97, 0x8c, 0x55, 0x09, 0x4a, 0xff, 0xda, 0x83, 0xae, 0xb3, + 0x06, 0xba, 0xf1, 0x51, 0x90, 0x73, 0x95, 0xa8, 0x52, 0x4c, 0xb4, 0x41, 0x76, 0xfe, 0xa7, 0xe1, + 0xe6, 0x7f, 0x4c, 0xfe, 0x61, 0xce, 0xce, 0x3f, 0xdc, 0x82, 0x56, 0x51, 0xb2, 0x6c, 0x3a, 0xaa, + 0x01, 0x57, 0xd4, 0xe9, 0xe4, 0x02, 0x09, 0xe9, 0x0c, 0x93, 0x28, 0xc9, 0x54, 0x45, 0x4f, 0x0e, + 0xe8, 0x1d, 0x68, 0x5b, 0xf8, 0xb8, 0x8d, 0x98, 0xf1, 0xb3, 0x24, 0x7b, 0xa6, 0xd3, 0x50, 0x6a, + 0x68, 0xca, 0x28, 0x8d, 0xa2, 0x8c, 0x42, 0xff, 0xc6, 0x83, 0x2e, 0x4a, 0x4a, 0x18, 0x8f, 0x0f, + 0x92, 0x28, 0x1c, 0xce, 0x84, 0xc4, 0x68, 0xa1, 0x18, 0x8c, 0x58, 0xc4, 0x03, 0x23, 0x31, 0x2e, + 0x18, 0x6d, 0xbe, 0xf6, 0xe2, 0x95, 0xbc, 0x98, 0x31, 0x4a, 0x3e, 0xda, 0xae, 0xa3, 0x20, 0x67, + 0xd2, 0xed, 0x57, 0xba, 0xda, 0x01, 0xa2, 0xfa, 0x40, 0x40, 0x16, 0x70, 0x36, 0x98, 0x84, 0x51, + 0x14, 0x4a, 0x5c, 0x29, 0xe1, 0x75, 0x53, 0xf4, 0x6f, 0x1b, 0xd0, 0x56, 0x6a, 0xe2, 0xe1, 0x68, + 0x2c, 0x33, 0xaa, 0xca, 0x11, 0x31, 0xcf, 0xcf, 0x82, 0xe8, 0x79, 0xc7, 0x75, 0xb1, 0x20, 0xe5, + 0x6b, 0x9d, 0xab, 0x5e, 0xeb, 0x15, 0x68, 0xa1, 0x78, 0xdd, 0x16, 0x3e, 0x92, 0xac, 0x70, 0x17, + 0x00, 0x3d, 0xbb, 0x2b, 0x66, 0xe7, 0x8b, 0x59, 0x01, 0x70, 0xbc, 0xa2, 0x85, 0x92, 0x57, 0xf4, + 0x3e, 0x74, 0x14, 0x19, 0xc1, 0x77, 0x11, 0x54, 0x15, 0x02, 0xee, 0xdc, 0x89, 0xef, 0x60, 0xea, + 0x2f, 0x77, 0xf5, 0x97, 0x4b, 0xaf, 0xfb, 0x52, 0x63, 0xd2, 0x8b, 0xb0, 0xae, 0x98, 0xf7, 0x38, + 0x0b, 0xd2, 0x13, 0xad, 0x7a, 0x47, 0xa6, 0x8c, 0x2a, 0xc0, 0xe4, 0x06, 0xcc, 0xe3, 0x67, 0x5a, + 0xfb, 0xd5, 0x3f, 0x3a, 0x89, 0x42, 0xb6, 0x61, 0x9e, 0x8d, 0xc6, 0x4c, 0x7b, 0xe6, 0xc4, 0x8d, + 0x91, 0xf0, 0x8e, 0x7c, 0x89, 0x80, 0x2a, 0x00, 0xa1, 0x25, 0x15, 0xe0, 0x6a, 0xce, 0x05, 0x1c, + 0x7e, 0x34, 0xa2, 0x1b, 0x40, 0x9e, 0x48, 0xa9, 0xb5, 0xb3, 0x80, 0xbf, 0x37, 0x07, 0x6d, 0x0b, + 0x8c, 0xaf, 0x79, 0x8c, 0x1b, 0x1e, 0x8c, 0xc2, 0x60, 0xc2, 0x38, 0xcb, 0x94, 0xa4, 0x96, 0xa0, + 0x42, 0xc1, 0x9e, 0x8e, 0x07, 0xc9, 0x94, 0x0f, 0x46, 0x6c, 0x9c, 0x31, 0x69, 0xd0, 0x3c, 0xbf, + 0x04, 0x45, 0xbc, 0x49, 0xf0, 0xdc, 0xc6, 0x93, 0xf2, 0x50, 0x82, 0xea, 0x9c, 0x9e, 0xe4, 0x51, + 0xb3, 0xc8, 0xe9, 0x49, 0x8e, 0x94, 0xf5, 0xd0, 0x7c, 0x8d, 0x1e, 0x7a, 0x0f, 0x36, 0xa5, 0xc6, + 0x51, 0x6f, 0x73, 0x50, 0x12, 0x93, 0x73, 0x66, 0xc9, 0x0d, 0x58, 0xc5, 0x3d, 0x6b, 0x01, 0xcf, + 0xc3, 0xef, 0xca, 0x68, 0xdc, 0xf3, 0x2b, 0x70, 0xc4, 0xc5, 0xe7, 0xe8, 0xe0, 0xca, 0x92, 0x43, + 0x05, 0x2e, 0x70, 0x83, 0xe7, 0x2e, 0x6e, 0x4b, 0xe1, 0x96, 0xe0, 0xb4, 0x0b, 0xed, 0x43, 0x9e, + 0xa4, 0xfa, 0x52, 0x96, 0xa1, 0x23, 0x87, 0xaa, 0xcc, 0x74, 0x19, 0x2e, 0x09, 0x29, 0x7a, 0x9a, + 0xa4, 0x49, 0x94, 0x8c, 0x67, 0x87, 0xd3, 0xa3, 0x7c, 0x98, 0x85, 0x29, 0x7a, 0xcc, 0xf4, 0xef, + 0x3d, 0x58, 0x77, 0x66, 0x55, 0xa8, 0xff, 0x8b, 0x52, 0xa4, 0x4d, 0x65, 0x40, 0x0a, 0xde, 0x9a, + 0xa5, 0x0e, 0x25, 0xa2, 0x4c, 0x9c, 0x7c, 0xaa, 0x8a, 0x05, 0x77, 0x61, 0x45, 0xef, 0x4c, 0x7f, + 0x28, 0xa5, 0xb0, 0x57, 0x95, 0x42, 0xf5, 0xfd, 0xb2, 0xfa, 0x40, 0x93, 0xf8, 0x15, 0xe9, 0x77, + 0xb2, 0x91, 0x38, 0xa3, 0x8e, 0xf9, 0xfa, 0xfa, 0x7b, 0xdb, 0xd9, 0xd5, 0x3b, 0x18, 0x1a, 0x60, + 0x4e, 0xff, 0xc8, 0x03, 0x28, 0x76, 0x87, 0x82, 0x51, 0xa8, 0x74, 0x4f, 0xe4, 0x4c, 0x2d, 0xf5, + 0x7d, 0x1d, 0x3a, 0x26, 0x33, 0x5d, 0x58, 0x89, 0xb6, 0x86, 0xa1, 0x87, 0xf2, 0x0e, 0xac, 0x8c, + 0xa3, 0xe4, 0x48, 0xd8, 0x5c, 0x51, 0xd1, 0xcc, 0x55, 0xb1, 0x6d, 0x59, 0x82, 0x1f, 0x29, 0x68, + 0x61, 0x52, 0x9a, 0x96, 0x49, 0xa1, 0x7f, 0xdc, 0x30, 0xf9, 0xd1, 0xe2, 0xcc, 0xe7, 0xbe, 0x32, + 0xb2, 0x5b, 0x51, 0x8e, 0xe7, 0xa4, 0x23, 0x45, 0x76, 0xe3, 0xe0, 0xb5, 0x81, 0xde, 0x1d, 0x58, + 0xce, 0xa4, 0xf6, 0xd1, 0xaa, 0xa9, 0xf9, 0x0a, 0xd5, 0xd4, 0xcd, 0x1c, 0xbb, 0xf3, 0x73, 0xb0, + 0x1a, 0x8c, 0x4e, 0x59, 0xc6, 0x43, 0xe1, 0xf1, 0x0b, 0xa3, 0x2f, 0x15, 0xea, 0x8a, 0x05, 0x17, + 0xb6, 0xf8, 0x1d, 0x58, 0x51, 0x05, 0x4e, 0x83, 0xa9, 0xfa, 0x56, 0x0a, 0x30, 0x22, 0xd2, 0xbf, + 0xd4, 0xa9, 0x58, 0xf7, 0x0e, 0xcf, 0xe7, 0x88, 0x7d, 0xba, 0x46, 0xe9, 0x74, 0x5f, 0x51, 0x69, + 0xd1, 0x91, 0x0e, 0x2b, 0x54, 0x82, 0x5a, 0x02, 0x55, 0x1a, 0xdb, 0x65, 0x69, 0xf3, 0x4d, 0x58, + 0x4a, 0x7f, 0x38, 0x07, 0x8b, 0x1f, 0xc5, 0xa7, 0x49, 0x38, 0x14, 0x49, 0xca, 0x09, 0x9b, 0x24, + 0xba, 0xcd, 0x00, 0x7f, 0xa3, 0x45, 0x17, 0x15, 0xb4, 0x94, 0xab, 0xec, 0xa1, 0x1e, 0xa2, 0x75, + 0xcb, 0x8a, 0xd6, 0x1a, 0x29, 0x29, 0x16, 0x04, 0xfd, 0xc3, 0xcc, 0xee, 0x16, 0x52, 0xa3, 0xa2, + 0x4f, 0x63, 0xde, 0xea, 0xd3, 0x10, 0x29, 0x6d, 0x59, 0x1c, 0x14, 0xec, 0x5c, 0xf2, 0xf5, 0x50, + 0xf8, 0xb1, 0x19, 0x93, 0x41, 0xaf, 0xb0, 0x93, 0x8b, 0xca, 0x8f, 0xb5, 0x81, 0x68, 0x4b, 0xe5, + 0x07, 0x12, 0x47, 0xea, 0x1a, 0x1b, 0x84, 0xbe, 0x45, 0xb9, 0xe1, 0xa8, 0x25, 0xaf, 0xb8, 0x04, + 0x46, 0x85, 0x34, 0x62, 0x46, 0x6f, 0xc8, 0x33, 0x80, 0x6c, 0x1d, 0x2a, 0xc3, 0x2d, 0x2f, 0x58, + 0x16, 0x39, 0xd5, 0x48, 0xf8, 0x20, 0x41, 0x14, 0x1d, 0x05, 0xc3, 0x67, 0xa2, 0x0d, 0x4c, 0xd4, + 0x34, 0x5b, 0xbe, 0x0b, 0xc4, 0x5d, 0x0f, 0x23, 0x7e, 0x3a, 0x50, 0x24, 0xba, 0xb2, 0x26, 0x69, + 0x81, 0xe8, 0x67, 0x40, 0xee, 0x8e, 0x46, 0xea, 0x86, 0x4c, 0x8c, 0x50, 0xf0, 0xd6, 0x73, 0x78, + 0x5b, 0x73, 0xc6, 0x46, 0xed, 0x19, 0xe9, 0x43, 0x68, 0x1f, 0x58, 0xdd, 0x5b, 0xe2, 0x32, 0x75, + 0xdf, 0x96, 0x12, 0x00, 0x0b, 0x62, 0x2d, 0xd8, 0xb0, 0x17, 0xa4, 0xbf, 0x04, 0x64, 0x3f, 0xcc, + 0xb9, 0xd9, 0x9f, 0x09, 0x15, 0x4d, 0xc6, 0xcb, 0x0a, 0x15, 0x15, 0x4c, 0x84, 0x8a, 0x77, 0x65, + 0x69, 0xb4, 0x7c, 0xb0, 0x1b, 0xb0, 0x14, 0x4a, 0x90, 0xd6, 0xc3, 0xcb, 0x4a, 0x80, 0x35, 0xa6, + 0x99, 0x47, 0x87, 0x42, 0x01, 0x1d, 0x35, 0xff, 0x7d, 0x0f, 0x16, 0xd5, 0xd1, 0xd0, 0x1c, 0x3a, + 0x7d, 0x6b, 0xf2, 0x60, 0x0e, 0xac, 0xbe, 0x6f, 0xa8, 0x2a, 0x75, 0x73, 0x75, 0x52, 0x47, 0xa0, + 0x99, 0x06, 0xfc, 0x44, 0x78, 0xd0, 0x2d, 0x5f, 0xfc, 0xd6, 0x91, 0xd2, 0xbc, 0x89, 0x94, 0x74, + 0x19, 0x58, 0x6d, 0xca, 0x64, 0x2d, 0xef, 0xc9, 0x32, 0x70, 0x01, 0x2e, 0x78, 0xa0, 0x36, 0x58, + 0xe6, 0x81, 0x42, 0xf5, 0xcd, 0x3c, 0xed, 0x43, 0xef, 0x01, 0x8b, 0x18, 0x67, 0x77, 0xa3, 0xa8, + 0x4c, 0xff, 0x32, 0x5c, 0xaa, 0x99, 0x53, 0x96, 0xf2, 0x11, 0xac, 0x3d, 0x60, 0x47, 0xd3, 0xf1, + 0x3e, 0x3b, 0x2d, 0xca, 0x0a, 0x04, 0x9a, 0xf9, 0x49, 0x72, 0xa6, 0xee, 0x4b, 0xfc, 0x26, 0x5f, + 0x02, 0x88, 0x10, 0x67, 0x90, 0xa7, 0x6c, 0xa8, 0x9b, 0x5e, 0x04, 0xe4, 0x30, 0x65, 0x43, 0xfa, + 0x1e, 0x10, 0x9b, 0x8e, 0x3a, 0x02, 0xbe, 0xc6, 0xe9, 0xd1, 0x20, 0x9f, 0xe5, 0x9c, 0x4d, 0x74, + 0x37, 0x8f, 0x0d, 0xa2, 0xef, 0x40, 0xe7, 0x20, 0x98, 0xf9, 0xec, 0x0b, 0xd5, 0x0e, 0x88, 0x01, + 0x59, 0x30, 0x43, 0xf1, 0x34, 0x01, 0x99, 0x98, 0xa6, 0x7f, 0xd7, 0x80, 0x05, 0x89, 0x89, 0x54, + 0x47, 0x2c, 0xe7, 0x61, 0x2c, 0xd3, 0xea, 0x8a, 0xaa, 0x05, 0xaa, 0xdc, 0x77, 0xa3, 0xe6, 0xbe, + 0x95, 0x8b, 0xa4, 0x1b, 0x04, 0xd4, 0xc5, 0x3a, 0x30, 0x11, 0x6f, 0x86, 0x13, 0x26, 0xbb, 0x42, + 0x9b, 0x2a, 0xde, 0xd4, 0x80, 0x52, 0xe4, 0x5b, 0xbc, 0x79, 0xb9, 0x3f, 0x2d, 0x88, 0xca, 0x2c, + 0xd8, 0xa0, 0x5a, 0xcd, 0xb2, 0x28, 0xfb, 0xff, 0x2a, 0x9a, 0xa5, 0xa2, 0x41, 0x96, 0xde, 0x40, + 0x83, 0x48, 0xbf, 0xc9, 0xd1, 0x20, 0x04, 0x56, 0x1f, 0x31, 0xe6, 0xb3, 0x34, 0xc9, 0x4c, 0x4f, + 0xe5, 0x0f, 0x3c, 0x58, 0x55, 0x16, 0xc1, 0xcc, 0x91, 0xeb, 0x8e, 0xf9, 0xf0, 0xea, 0x32, 0xad, + 0x6f, 0x41, 0x57, 0x04, 0x50, 0x18, 0x1d, 0x89, 0x68, 0x49, 0xe5, 0x14, 0x1c, 0x20, 0xee, 0x49, + 0xe7, 0x0e, 0x27, 0x61, 0xa4, 0x18, 0x6c, 0x83, 0xd0, 0xd4, 0xe9, 0x00, 0x4b, 0xb0, 0xd7, 0xf3, + 0xcd, 0x98, 0x1e, 0xc0, 0x9a, 0xb5, 0x5f, 0x25, 0x50, 0x77, 0x40, 0x57, 0x25, 0x65, 0x8a, 0x40, + 0xbe, 0x8b, 0x2d, 0xd7, 0xb8, 0x15, 0x9f, 0x39, 0xc8, 0xf4, 0x9f, 0x3d, 0x58, 0x97, 0x86, 0x5e, + 0xb9, 0x51, 0xa6, 0x91, 0x69, 0x41, 0x7a, 0x36, 0x52, 0xe0, 0xf7, 0x2e, 0xf8, 0x6a, 0x4c, 0xbe, + 0xf6, 0x86, 0xce, 0x89, 0x29, 0x00, 0x9e, 0xc3, 0x9e, 0xb9, 0x3a, 0xf6, 0xbc, 0xe2, 0xf0, 0x75, + 0x01, 0xf0, 0x7c, 0x6d, 0x00, 0x7c, 0x6f, 0x11, 0xe6, 0xf3, 0x61, 0x92, 0x32, 0xba, 0x09, 0x1b, + 0xee, 0xe1, 0x24, 0xcb, 0x76, 0xff, 0xc5, 0x83, 0x65, 0x99, 0x8c, 0x93, 0xdd, 0xda, 0x2c, 0x23, + 0x18, 0x6b, 0x59, 0x4d, 0xe0, 0xc4, 0xb8, 0x9a, 0xd5, 0x66, 0xf2, 0xfe, 0xe5, 0xda, 0x39, 0xed, + 0x67, 0x7f, 0xef, 0xc7, 0xff, 0xfe, 0x27, 0x8d, 0x8b, 0x74, 0x75, 0xe7, 0xf4, 0xf6, 0x8e, 0x50, + 0x89, 0xec, 0x4c, 0x60, 0x7c, 0xe0, 0xdd, 0xc0, 0x55, 0xec, 0xfe, 0x70, 0xb3, 0x4a, 0x4d, 0x9f, + 0xb9, 0x59, 0xa5, 0xb6, 0xa1, 0xdc, 0x59, 0x65, 0x2a, 0x30, 0xcc, 0x2a, 0xbb, 0xff, 0xdd, 0x87, + 0x96, 0x09, 0x0a, 0xc9, 0x77, 0xa0, 0xeb, 0x24, 0x1e, 0x89, 0x26, 0x5c, 0x97, 0xca, 0xec, 0x5f, + 0xa9, 0x9f, 0x54, 0xcb, 0x5e, 0x15, 0xcb, 0xf6, 0xc8, 0x26, 0x2e, 0xab, 0xb2, 0x7d, 0x3b, 0x22, + 0x23, 0x2b, 0x3b, 0x19, 0x9e, 0xc1, 0xb2, 0x9b, 0x2c, 0x24, 0x57, 0x5c, 0xd1, 0x28, 0xad, 0xf6, + 0xa5, 0x73, 0x66, 0xd5, 0x72, 0x57, 0xc4, 0x72, 0x9b, 0x64, 0xc3, 0x5e, 0xce, 0x04, 0x6b, 0x4c, + 0xf4, 0x9e, 0xd8, 0x8d, 0xe3, 0x44, 0xd3, 0xab, 0x6f, 0x28, 0xef, 0x5f, 0xaa, 0x36, 0x89, 0xab, + 0xae, 0x72, 0xda, 0x13, 0x4b, 0x11, 0x22, 0x18, 0x6a, 0xf7, 0x8d, 0x93, 0x6f, 0x43, 0xcb, 0xb4, + 0x9d, 0x92, 0x2d, 0xab, 0xd7, 0xd7, 0xee, 0x85, 0xed, 0xf7, 0xaa, 0x13, 0x75, 0x57, 0x65, 0x53, + 0x46, 0x81, 0xd8, 0x87, 0x8b, 0xca, 0x42, 0x1f, 0xb1, 0x9f, 0xe4, 0x24, 0x35, 0xed, 0xee, 0xb7, + 0x3c, 0x72, 0x07, 0x96, 0x74, 0x37, 0x2f, 0xd9, 0xac, 0xef, 0x4a, 0xee, 0x6f, 0x55, 0xe0, 0x4a, + 0x8f, 0xdc, 0x05, 0x28, 0x1a, 0x4f, 0x49, 0xef, 0xbc, 0xfe, 0x58, 0xc3, 0xc4, 0x9a, 0x2e, 0xd5, + 0xb1, 0xe8, 0xbb, 0x75, 0xfb, 0x5a, 0xc9, 0x97, 0x0b, 0xfc, 0xda, 0x8e, 0xd7, 0x57, 0x10, 0xa4, + 0x9b, 0x82, 0x77, 0xab, 0x64, 0x19, 0x79, 0x17, 0xb3, 0x33, 0xdd, 0x85, 0xf5, 0x00, 0xda, 0x56, + 0x33, 0x2b, 0xd1, 0x14, 0xaa, 0x8d, 0xb0, 0xfd, 0x7e, 0xdd, 0x94, 0xda, 0xee, 0xaf, 0x41, 0xd7, + 0xe9, 0x4a, 0x35, 0x2f, 0xa3, 0xae, 0xe7, 0xd5, 0xbc, 0x8c, 0xfa, 0x46, 0xd6, 0x6f, 0x41, 0xdb, + 0xea, 0x21, 0x25, 0x56, 0x45, 0xbb, 0xd4, 0x23, 0x6a, 0x76, 0x54, 0xd3, 0x72, 0x4a, 0x37, 0xc4, + 0x79, 0x97, 0x69, 0x0b, 0xcf, 0x2b, 0x5a, 0x91, 0x50, 0x48, 0xbe, 0x03, 0xcb, 0x6e, 0xef, 0xa8, + 0x79, 0x55, 0xb5, 0x5d, 0xa8, 0xe6, 0x55, 0x9d, 0xd3, 0x70, 0xaa, 0x04, 0xf2, 0xc6, 0xba, 0x59, + 0x64, 0xe7, 0x85, 0x4a, 0x89, 0xbe, 0x24, 0xdf, 0x44, 0xd5, 0xa1, 0x7a, 0xc3, 0x48, 0xd1, 0x4b, + 0xeb, 0x76, 0x90, 0x19, 0x69, 0xaf, 0xb4, 0x91, 0xd1, 0x35, 0x41, 0xbc, 0x4d, 0x8a, 0x13, 0x90, + 0x8f, 0x61, 0x51, 0xf5, 0x88, 0x91, 0x8b, 0x85, 0x54, 0x5b, 0x09, 0xa4, 0xfe, 0x66, 0x19, 0xac, + 0x88, 0xad, 0x0b, 0x62, 0x5d, 0xd2, 0x46, 0x62, 0x63, 0xc6, 0x43, 0xa4, 0x11, 0xc3, 0x4a, 0xa9, + 0x8a, 0x65, 0x1e, 0x4b, 0x7d, 0x0d, 0xbc, 0x7f, 0xf5, 0xd5, 0xc5, 0x2f, 0x57, 0xcd, 0x68, 0xf5, + 0xb2, 0xa3, 0x5b, 0x16, 0x7e, 0x13, 0x3a, 0x76, 0x4b, 0xa2, 0xd1, 0xd9, 0x35, 0xed, 0x8b, 0x46, + 0x67, 0xd7, 0xf5, 0x30, 0xea, 0xcb, 0x25, 0x1d, 0x7b, 0x19, 0xf2, 0x2d, 0x58, 0xb1, 0xea, 0xa5, + 0x87, 0xb3, 0x78, 0x68, 0x84, 0xa7, 0xda, 0xdd, 0xd2, 0xaf, 0xb3, 0xb4, 0x74, 0x4b, 0x10, 0x5e, + 0xa3, 0x0e, 0x61, 0x14, 0x9c, 0xfb, 0xd0, 0xb6, 0x6b, 0xb1, 0xaf, 0xa0, 0xbb, 0x65, 0x4d, 0xd9, + 0xcd, 0x1e, 0xb7, 0x3c, 0xf2, 0x67, 0x1e, 0x74, 0xec, 0xbe, 0x29, 0xe2, 0x64, 0x61, 0x4a, 0x74, + 0x7a, 0xf6, 0x9c, 0x4d, 0x88, 0x3e, 0x11, 0x9b, 0xdc, 0xbb, 0xf1, 0xc8, 0x61, 0xf2, 0x0b, 0xc7, + 0x89, 0xba, 0x69, 0xff, 0xd3, 0xe3, 0x65, 0x79, 0xd2, 0xee, 0xfe, 0x79, 0x79, 0xcb, 0x23, 0x1f, + 0xc8, 0xff, 0xf3, 0xe8, 0x80, 0x86, 0x58, 0x8a, 0xad, 0xcc, 0x2e, 0xfb, 0x4f, 0x32, 0xdb, 0xde, + 0x2d, 0x8f, 0xfc, 0x96, 0xfc, 0x73, 0x87, 0xfa, 0x56, 0x70, 0xfd, 0x4d, 0xbf, 0xa7, 0x6f, 0x89, + 0x93, 0x5c, 0xa5, 0x97, 0x9c, 0x93, 0x94, 0x35, 0xfb, 0x01, 0x40, 0x11, 0x9d, 0x92, 0x52, 0xa8, + 0x66, 0x74, 0x5e, 0x35, 0x80, 0x75, 0x6f, 0x53, 0x47, 0x74, 0x52, 0x0d, 0x74, 0xac, 0xb8, 0x30, + 0x37, 0xd7, 0x59, 0x8d, 0x32, 0xfb, 0xfd, 0xba, 0x29, 0x45, 0xff, 0x2b, 0x82, 0xfe, 0x97, 0xc8, + 0x65, 0x9b, 0xfe, 0xce, 0x0b, 0x3b, 0x2a, 0x7d, 0x49, 0x3e, 0x83, 0xee, 0x7e, 0x92, 0x3c, 0x9b, + 0xa6, 0x26, 0x01, 0xe2, 0xc6, 0x59, 0x18, 0x19, 0xf7, 0x4b, 0x87, 0xa2, 0xd7, 0x05, 0xe5, 0xcb, + 0xe4, 0x92, 0x4b, 0xb9, 0x88, 0x95, 0x5f, 0x92, 0x00, 0xd6, 0x8c, 0xbd, 0x33, 0x07, 0xe9, 0xbb, + 0x74, 0xec, 0x90, 0xb5, 0xb2, 0x86, 0xe3, 0x81, 0x98, 0x35, 0x72, 0x4d, 0xf3, 0x96, 0x47, 0x0e, + 0xa0, 0xf3, 0x80, 0x0d, 0x93, 0x11, 0x53, 0xa1, 0xd1, 0x7a, 0xb1, 0x73, 0x13, 0x53, 0xf5, 0xbb, + 0x0e, 0xd0, 0xd5, 0x00, 0x69, 0x30, 0xcb, 0xd8, 0x17, 0x3b, 0x2f, 0x54, 0xd0, 0xf5, 0x52, 0x6b, + 0x00, 0x1d, 0x28, 0x3a, 0x1a, 0xa0, 0x14, 0x59, 0x3a, 0x1a, 0xa0, 0x12, 0x59, 0x3a, 0x1a, 0x40, + 0x07, 0xaa, 0x24, 0xc2, 0x78, 0xb3, 0x14, 0x8c, 0x1a, 0xab, 0x79, 0x5e, 0x08, 0xdb, 0xbf, 0x76, + 0x3e, 0x82, 0xbb, 0xda, 0x0d, 0x77, 0xb5, 0x43, 0xe8, 0x3e, 0x60, 0x92, 0x59, 0xb2, 0xaa, 0xd0, + 0x77, 0x55, 0x8a, 0x5d, 0x81, 0x28, 0xab, 0x1b, 0x31, 0xe7, 0xaa, 0x78, 0x91, 0xd2, 0x27, 0xdf, + 0x86, 0xf6, 0x63, 0xc6, 0x75, 0x19, 0xc1, 0xf8, 0x1e, 0xa5, 0xba, 0x42, 0xbf, 0xa6, 0x0a, 0x41, + 0xaf, 0x09, 0x6a, 0x7d, 0xd2, 0x33, 0xd4, 0x76, 0xd8, 0x68, 0xcc, 0xe4, 0xe3, 0x1f, 0x84, 0xa3, + 0x97, 0xe4, 0xd7, 0x05, 0x71, 0x53, 0x79, 0xdc, 0xb4, 0xb2, 0xcf, 0x36, 0xf1, 0x95, 0x12, 0xbc, + 0x8e, 0x72, 0x9c, 0x8c, 0x98, 0x65, 0xec, 0x62, 0x68, 0x5b, 0x65, 0x66, 0xf3, 0xa0, 0xaa, 0xb5, + 0x6b, 0xf3, 0xa0, 0x6a, 0xaa, 0xd2, 0x74, 0x5b, 0xac, 0x43, 0xc9, 0xb5, 0x62, 0x1d, 0x59, 0x89, + 0x2e, 0x56, 0xda, 0x79, 0x11, 0x4c, 0xf8, 0x4b, 0xf2, 0xb9, 0x68, 0xa0, 0xb6, 0x4b, 0x25, 0x85, + 0xef, 0x53, 0xae, 0xaa, 0x18, 0x66, 0x59, 0x53, 0xae, 0x3f, 0x24, 0x97, 0x12, 0x36, 0xf1, 0x6b, + 0x00, 0x87, 0x3c, 0x49, 0x1f, 0x04, 0x6c, 0x92, 0xc4, 0x85, 0x26, 0x2b, 0xca, 0x01, 0x85, 0x26, + 0xb3, 0x6a, 0x02, 0xe4, 0x73, 0xcb, 0xfb, 0x74, 0x2a, 0x4d, 0x5a, 0xb8, 0xce, 0xad, 0x18, 0x18, + 0x86, 0xd4, 0x54, 0x0d, 0x6e, 0x79, 0xe8, 0x4b, 0x16, 0xa9, 0x0f, 0xe3, 0x4b, 0x56, 0xb2, 0x2a, + 0x46, 0x0d, 0xd6, 0xe4, 0x49, 0x0e, 0xa0, 0x55, 0xc4, 0xdf, 0xda, 0x3c, 0x95, 0xa3, 0x75, 0x63, + 0x6f, 0x2a, 0x61, 0x31, 0x5d, 0x15, 0xac, 0x02, 0xb2, 0x84, 0xac, 0x12, 0x95, 0xf2, 0x21, 0xac, + 0xcb, 0x0d, 0x1a, 0xe3, 0x29, 0x12, 0xdc, 0xfa, 0x24, 0x35, 0x61, 0xb0, 0x79, 0xcd, 0x75, 0x51, + 0xa4, 0xf6, 0x4e, 0xa8, 0x59, 0xe1, 0x03, 0xef, 0xc6, 0xd1, 0x82, 0xf8, 0x1b, 0xf2, 0x57, 0xff, + 0x27, 0x00, 0x00, 0xff, 0xff, 0x33, 0xf0, 0xc8, 0xda, 0xb8, 0x3c, 0x00, 0x00, } diff --git a/lnrpc/rpc.proto b/lnrpc/rpc.proto index 6fca5c07..9acf5122 100644 --- a/lnrpc/rpc.proto +++ b/lnrpc/rpc.proto @@ -877,6 +877,9 @@ message OpenChannelRequest { /// Whether this channel should be private, not announced to the greater network. bool private = 8 [json_name = "private"]; + + /// The minimum value in millisatoshi we will require for incoming HTLCs on the channel. + int64 min_htlc_msat = 9 [json_name = "min_htlc_msat"]; } message OpenStatusUpdate { oneof update { diff --git a/lnrpc/rpc.swagger.json b/lnrpc/rpc.swagger.json index aecdc2cf..a5cf060f 100644 --- a/lnrpc/rpc.swagger.json +++ b/lnrpc/rpc.swagger.json @@ -1589,6 +1589,11 @@ "type": "boolean", "format": "boolean", "description": "/ Whether this channel should be private, not announced to the greater network." + }, + "min_htlc_msat": { + "type": "string", + "format": "int64", + "description": "/ The minimum value in millisatoshi we will require for incoming HTLCs on the channel." } } }, From 59b331c04d38b84c88992539397e9d8f851588cc Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sat, 16 Dec 2017 23:57:31 +0100 Subject: [PATCH 11/39] lncli: add min_htlc_msat to openchannel --- cmd/lncli/commands.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/lncli/commands.go b/cmd/lncli/commands.go index 94d511ed..5e553b3d 100644 --- a/cmd/lncli/commands.go +++ b/cmd/lncli/commands.go @@ -431,6 +431,11 @@ var openChannelCommand = cli.Command{ "must be explicitly told about it to be able " + "to route through it", }, + cli.Int64Flag{ + Name: "min_htlc_msat", + Usage: "(optional) the minimum value we will require " + + "for incoming HTLCs on the channel", + }, }, Action: actionDecorator(openChannel), } @@ -456,8 +461,9 @@ func openChannel(ctx *cli.Context) error { } req := &lnrpc.OpenChannelRequest{ - TargetConf: int32(ctx.Int64("conf_target")), - SatPerByte: ctx.Int64("sat_per_byte"), + TargetConf: int32(ctx.Int64("conf_target")), + SatPerByte: ctx.Int64("sat_per_byte"), + MinHtlcMsat: ctx.Int64("min_htlc_msat"), } switch { From 84e5adcdd070cd7cb5652f2f355132569bd3cc1d Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sun, 17 Dec 2017 00:00:37 +0100 Subject: [PATCH 12/39] server: add min_htlc_msat parameter to OpenChannel method --- server.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server.go b/server.go index 7d9f1dd0..163c6c97 100644 --- a/server.go +++ b/server.go @@ -1495,6 +1495,8 @@ type openChanReq struct { private bool + minHtlc lnwire.MilliSatoshi + // TODO(roasbeef): add ability to specify channel constraints as well updates chan *lnrpc.OpenStatusUpdate @@ -1612,6 +1614,7 @@ func (s *server) DisconnectPeer(pubKey *btcec.PublicKey) error { // NOTE: This function is safe for concurrent access. func (s *server) OpenChannel(peerID int32, nodeKey *btcec.PublicKey, localAmt btcutil.Amount, pushAmt lnwire.MilliSatoshi, + minHtlc lnwire.MilliSatoshi, fundingFeePerByte btcutil.Amount, private bool) (chan *lnrpc.OpenStatusUpdate, chan error) { @@ -1674,6 +1677,7 @@ func (s *server) OpenChannel(peerID int32, nodeKey *btcec.PublicKey, fundingFeePerWeight: fundingFeePerWeight, pushAmt: pushAmt, private: private, + minHtlc: minHtlc, updates: updateChan, err: errChan, } From f3dff2ae974f0616bc838d5f7140032aef0be8c3 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sun, 17 Dec 2017 00:00:11 +0100 Subject: [PATCH 13/39] rpcserver: add min_htlc_msat parameter to OpenChannel and OpenChannelSync --- rpcserver.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rpcserver.go b/rpcserver.go index 0f6495b8..744690e0 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -551,6 +551,7 @@ func (r *rpcServer) OpenChannel(in *lnrpc.OpenChannelRequest, localFundingAmt := btcutil.Amount(in.LocalFundingAmount) remoteInitialBalance := btcutil.Amount(in.PushSat) + minHtlc := lnwire.MilliSatoshi(in.MinHtlcMsat) // Ensure that the initial balance of the remote party (if pushing // satoshis) does not exceed the amount the local party has requested @@ -627,7 +628,7 @@ func (r *rpcServer) OpenChannel(in *lnrpc.OpenChannelRequest, updateChan, errChan := r.server.OpenChannel( in.TargetPeerId, nodePubKey, localFundingAmt, lnwire.NewMSatFromSatoshis(remoteInitialBalance), - feePerByte, in.Private, + minHtlc, feePerByte, in.Private, ) var outpoint wire.OutPoint @@ -722,6 +723,7 @@ func (r *rpcServer) OpenChannelSync(ctx context.Context, localFundingAmt := btcutil.Amount(in.LocalFundingAmount) remoteInitialBalance := btcutil.Amount(in.PushSat) + minHtlc := lnwire.MilliSatoshi(in.MinHtlcMsat) // Ensure that the initial balance of the remote party (if pushing // satoshis) does not exceed the amount the local party has requested @@ -746,7 +748,7 @@ func (r *rpcServer) OpenChannelSync(ctx context.Context, updateChan, errChan := r.server.OpenChannel( in.TargetPeerId, nodepubKey, localFundingAmt, lnwire.NewMSatFromSatoshis(remoteInitialBalance), - feePerByte, in.Private, + minHtlc, feePerByte, in.Private, ) select { From 7805a53c34d15f46bd485e3b60c817a7232ffd7b Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sun, 17 Dec 2017 00:01:08 +0100 Subject: [PATCH 14/39] pilot: pass minHtlc = 1 satoshi to OpenChannel --- pilot.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pilot.go b/pilot.go index ad239ef6..82abe7ef 100644 --- a/pilot.go +++ b/pilot.go @@ -89,8 +89,12 @@ func (c *chanController) OpenChannel(target *btcec.PublicKey, if err != nil { return err } + + // TODO(halseth): make configurable? + minHtlc := lnwire.NewMSatFromSatoshis(1) + updateStream, errChan := c.server.OpenChannel(-1, target, amt, 0, - feePerWeight, false) + minHtlc, feePerWeight, false) select { case err := <-errChan: From 79a6efe33f6191936573cb692d97863a39ff81e4 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sun, 17 Dec 2017 00:01:38 +0100 Subject: [PATCH 15/39] fundingmanager: use minHtlc from initFundingMsg This commit makes the fundingmanager read the minHtlc field of the initFundingMsg, and add it to the reservation as this node's htlc_minimum_msat for the open_channel message. If the field is not specified in the initFundingMsg, the default value found in the DefaultRoutingPolicy will be used. --- fundingmanager.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fundingmanager.go b/fundingmanager.go index b64b8854..75ba76d6 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -2101,6 +2101,8 @@ func (f *fundingManager) newChanAnnouncement(localPubKey, remotePubKey *btcec.Pu chanFlags = 1 } + // We announce the channel with the default values. Some of + // these values can later be changed by crafting a new ChannelUpdate. chanUpdateAnn := &lnwire.ChannelUpdate{ ShortChannelID: shortChanID, ChainHash: chainHash, @@ -2252,6 +2254,7 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { remoteAmt = msg.remoteFundingAmt capacity = localAmt + remoteAmt ourDustLimit = lnwallet.DefaultDustLimit() + minHtlc = msg.minHtlc ) fndgLog.Infof("Initiating fundingRequest(localAmt=%v, remoteAmt=%v, "+ @@ -2321,9 +2324,14 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { // delay we require given the total amount of funds within the channel. remoteCsvDelay := f.cfg.RequiredRemoteDelay(capacity) + // If no minimum HTLC value was specified, use the default one. + if minHtlc == 0 { + minHtlc = f.cfg.DefaultRoutingPolicy.MinHTLC + } + // Once the reservation has been created, and indexed, queue a funding // request to the remote peer, kicking off the funding workflow. - reservation.RegisterMinHTLC(f.cfg.DefaultRoutingPolicy.MinHTLC) + reservation.RegisterMinHTLC(minHtlc) ourContribution := reservation.OurContribution() // Finally, we'll use the current value of the channels and our default From f4feb1e6975d4da2632cf6918e4fc33ca7d77442 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sun, 17 Dec 2017 14:31:38 +0100 Subject: [PATCH 16/39] config: add forwarding policy rules to config This commit moves the forwarding policy rules for Bitcoin and Litecoin, previously defined in the chainregistry, to config.go, making them possible to define by the user. We validate that the TimeLockDelta set is at least 4, the other rules we let the user specify arbitrarily, even 0. --- config.go | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/config.go b/config.go index d0751d1c..98517752 100644 --- a/config.go +++ b/config.go @@ -41,6 +41,20 @@ const ( defaultNumChanConfs = 3 defaultNoEncryptWallet = false defaultTrickleDelay = 30 * 1000 + + // minTimeLockDelta is the minimum timelock we require for incoming + // HTLCs on our channels. + minTimeLockDelta = 4 + + defaultBitcoinMinHTLCMSat = 1000 + defaultBitcoinBaseFeeMSat = 1000 + defaultBitcoinFeeRate = 1 + defaultBitcoinTimeLockDelta = 144 + + defaultLitecoinMinHTLCMSat = 1000 + defaultLitecoinBaseFeeMSat = 1000 + defaultLitecoinFeeRate = 1 + defaultLitecoinTimeLockDelta = 576 ) var ( @@ -74,6 +88,11 @@ type chainConfig struct { TestNet3 bool `long:"testnet" description:"Use the test network"` SimNet bool `long:"simnet" description:"Use the simulation test network"` RegTest bool `long:"regtest" description:"Use the regression test network"` + + MinHTLC lnwire.MilliSatoshi `long:"minhtlc" description:"The smallest HTLC we are willing to forward on our channels, in millisatoshi"` + BaseFee lnwire.MilliSatoshi `long:"basefee" description:"The base fee in millisatoshi we will charge for forwarding payments on our channels"` + FeeRate lnwire.MilliSatoshi `long:"feerate" description:"The fee rate used when forwarding payments on our channels. The total fee charged is basefee + (amount * feerate / 1000000), where amount is the forwarded amount."` + TimeLockDelta uint32 `long:"timelockdelta" description:"The CLTV delta we will subtract from a forwarded HTLC's timelock value"` } type neutrinoConfig struct { @@ -165,12 +184,20 @@ func loadConfig() (*config, error) { DefaultNumChanConfs: defaultNumChanConfs, NoEncryptWallet: defaultNoEncryptWallet, Bitcoin: &chainConfig{ - RPCHost: defaultRPCHost, - RPCCert: defaultBtcdRPCCertFile, + RPCHost: defaultRPCHost, + RPCCert: defaultBtcdRPCCertFile, + MinHTLC: defaultBitcoinMinHTLCMSat, + BaseFee: defaultBitcoinBaseFeeMSat, + FeeRate: defaultBitcoinFeeRate, + TimeLockDelta: defaultBitcoinTimeLockDelta, }, Litecoin: &chainConfig{ - RPCHost: defaultRPCHost, - RPCCert: defaultLtcdRPCCertFile, + RPCHost: defaultRPCHost, + RPCCert: defaultLtcdRPCCertFile, + MinHTLC: defaultLitecoinMinHTLCMSat, + BaseFee: defaultLitecoinBaseFeeMSat, + FeeRate: defaultLitecoinFeeRate, + TimeLockDelta: defaultLitecoinTimeLockDelta, }, Autopilot: &autoPilotConfig{ MaxChannels: 5, @@ -256,6 +283,11 @@ func loadConfig() (*config, error) { return nil, fmt.Errorf(str, funcName) } + if cfg.Litecoin.TimeLockDelta < minTimeLockDelta { + return nil, fmt.Errorf("timelockdelta must be at least %v", + minTimeLockDelta) + } + // The litecoin chain is the current active chain. However // throughout the codebase we required chiancfg.Params. So as a // temporary hack, we'll mutate the default net params for @@ -305,6 +337,11 @@ func loadConfig() (*config, error) { return nil, err } + if cfg.Bitcoin.TimeLockDelta < minTimeLockDelta { + return nil, fmt.Errorf("timelockdelta must be at least %v", + minTimeLockDelta) + } + if !cfg.NeutrinoMode.Active { // If needed, we'll attempt to automatically configure // the RPC control plan for the target btcd node. From 1f6268cdde14873e9afeb3d25e0878373a455fe0 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sun, 17 Dec 2017 14:33:09 +0100 Subject: [PATCH 17/39] rpcserver: use cfg.Bitcoin.TimeLockDelta as default CLTVExpiry for invoices --- rpcserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcserver.go b/rpcserver.go index 744690e0..bf8e9908 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -2085,7 +2085,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context, zpay32.CLTVExpiry(invoice.CltvExpiry)) default: // TODO(roasbeef): assumes set delta between versions - defaultDelta := defaultBitcoinForwardingPolicy.TimeLockDelta + defaultDelta := cfg.Bitcoin.TimeLockDelta options = append(options, zpay32.CLTVExpiry(uint64(defaultDelta))) } From f1c6d40db85c4bba2427e974bcd114b7851f7856 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 20 Dec 2017 17:31:46 +0100 Subject: [PATCH 18/39] lnd_test: use defaultBitcoinTimeLockDelta instead of defaultBitcoinForwardingPolicy.TimeLockDelta --- lnd_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnd_test.go b/lnd_test.go index 0292e278..a9a0ab72 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -1095,7 +1095,7 @@ func testChannelForceClosure(net *lntest.NetworkHarness, t *harnessTest) { // TODO(roasbeef): should check default value in config here // instead, or make delay a param defaultCSV := uint32(4) - defaultCLTV := defaultBitcoinForwardingPolicy.TimeLockDelta + defaultCLTV := uint32(defaultBitcoinTimeLockDelta) // Since we'd like to test failure scenarios with outstanding htlcs, // we'll introduce another node into our test network: Carol. From 10bd46ac72b91863903bcc519f5f23cc4f48a24a Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sun, 17 Dec 2017 14:33:49 +0100 Subject: [PATCH 19/39] chainregistry: use forwarding policy rules from config This commit removes the definitions of defaultBitcoinForwardingPolicy and defaultLitecoinForwardingPolicy from the the chainregistry, and instead creates a routingPolicy from the values found in the config. --- chainregistry.go | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/chainregistry.go b/chainregistry.go index 93cc42bc..675ad5cb 100644 --- a/chainregistry.go +++ b/chainregistry.go @@ -18,7 +18,6 @@ import ( "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/btcwallet" - "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/chainview" "github.com/roasbeef/btcd/chaincfg/chainhash" "github.com/roasbeef/btcd/rpcclient" @@ -27,28 +26,11 @@ import ( "github.com/roasbeef/btcwallet/walletdb" ) -// defaultBitcoinForwardingPolicy is the default forwarding policy used for -// Bitcoin channels. -var defaultBitcoinForwardingPolicy = htlcswitch.ForwardingPolicy{ - MinHTLC: lnwire.NewMSatFromSatoshis(1), - BaseFee: lnwire.NewMSatFromSatoshis(1), - FeeRate: 1, - TimeLockDelta: 144, -} - -// defaultLitecoinForwardingPolicy is the default forwarding policy used for -// Litecoin channels. -var defaultLitecoinForwardingPolicy = htlcswitch.ForwardingPolicy{ - MinHTLC: lnwire.NewMSatFromSatoshis(1), - BaseFee: 1, - FeeRate: 1, - TimeLockDelta: 576, -} - // defaultChannelConstraints is the default set of channel constraints that are // meant to be used when initially funding a channel. // // TODO(roasbeef): have one for both chains +// TODO(halseth): make configurable at startup? var defaultChannelConstraints = channeldb.ChannelConstraints{ DustLimit: lnwallet.DefaultDustLimit(), MaxAcceptedHtlcs: lnwallet.MaxHTLCNumber / 2, @@ -119,12 +101,22 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB, switch registeredChains.PrimaryChain() { case bitcoinChain: - cc.routingPolicy = defaultBitcoinForwardingPolicy + cc.routingPolicy = htlcswitch.ForwardingPolicy{ + MinHTLC: cfg.Bitcoin.MinHTLC, + BaseFee: cfg.Bitcoin.BaseFee, + FeeRate: cfg.Bitcoin.FeeRate, + TimeLockDelta: cfg.Bitcoin.TimeLockDelta, + } cc.feeEstimator = lnwallet.StaticFeeEstimator{ FeeRate: 50, } case litecoinChain: - cc.routingPolicy = defaultLitecoinForwardingPolicy + cc.routingPolicy = htlcswitch.ForwardingPolicy{ + MinHTLC: cfg.Litecoin.MinHTLC, + BaseFee: cfg.Litecoin.BaseFee, + FeeRate: cfg.Litecoin.FeeRate, + TimeLockDelta: cfg.Litecoin.TimeLockDelta, + } cc.feeEstimator = lnwallet.StaticFeeEstimator{ FeeRate: 100, } From 633878d497bace5efb0c36ba176907864a49c671 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 19 Dec 2017 17:29:45 +0100 Subject: [PATCH 20/39] funding test: return static num conf instead of default value --- fundingmanager_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fundingmanager_test.go b/fundingmanager_test.go index 692bc8a9..d5ce818e 100644 --- a/fundingmanager_test.go +++ b/fundingmanager_test.go @@ -264,8 +264,7 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey, }, NumRequiredConfs: func(chanAmt btcutil.Amount, pushAmt lnwire.MilliSatoshi) uint16 { - - return uint16(cfg.DefaultNumChanConfs) + return 3 }, RequiredRemoteDelay: func(amt btcutil.Amount) uint16 { return 4 From 0815bd4eb0c72b6104963622c038928dc1bb8941 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sun, 17 Dec 2017 16:07:32 +0100 Subject: [PATCH 21/39] config: make DefaultNumChanConfs part of chainConfig This commit moves the definition of DefaultNumChanConfs into the chainConfig (such that it is set as e.g. "--bitcoin.defaultchanconfs"), making it possible to set individually for different chains. It also adds the flag DefaultRemoteDelay to the chainConfig, which can be used to set the CSV delay we will require the remote to wait before retrieving its own funds in case of an uncooperative close of the channel. Both these are set 0 by default (if not specified by the user), which in that case we will dynamically set the values, scaling them according to the channel size. --- config.go | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/config.go b/config.go index 98517752..4ce84bed 100644 --- a/config.go +++ b/config.go @@ -38,7 +38,6 @@ const ( defaultPeerPort = 9735 defaultRPCHost = "localhost" defaultMaxPendingChannels = 1 - defaultNumChanConfs = 3 defaultNoEncryptWallet = false defaultTrickleDelay = 30 * 1000 @@ -89,10 +88,12 @@ type chainConfig struct { SimNet bool `long:"simnet" description:"Use the simulation test network"` RegTest bool `long:"regtest" description:"Use the regression test network"` - MinHTLC lnwire.MilliSatoshi `long:"minhtlc" description:"The smallest HTLC we are willing to forward on our channels, in millisatoshi"` - BaseFee lnwire.MilliSatoshi `long:"basefee" description:"The base fee in millisatoshi we will charge for forwarding payments on our channels"` - FeeRate lnwire.MilliSatoshi `long:"feerate" description:"The fee rate used when forwarding payments on our channels. The total fee charged is basefee + (amount * feerate / 1000000), where amount is the forwarded amount."` - TimeLockDelta uint32 `long:"timelockdelta" description:"The CLTV delta we will subtract from a forwarded HTLC's timelock value"` + DefaultNumChanConfs int `long:"defaultchanconfs" description:"The default number of confirmations a channel must have before it's considered open. If this is not set, we will scale the value according to the channel size."` + DefaultRemoteDelay int `long:"defaultremotedelay" description:"The default number of blocks we will require our channel counterparty to wait before accessing its funds in case of unilateral close. If this is not set, we will scale the value according to the channel size."` + MinHTLC lnwire.MilliSatoshi `long:"minhtlc" description:"The smallest HTLC we are willing to forward on our channels, in millisatoshi"` + BaseFee lnwire.MilliSatoshi `long:"basefee" description:"The base fee in millisatoshi we will charge for forwarding payments on our channels"` + FeeRate lnwire.MilliSatoshi `long:"feerate" description:"The fee rate used when forwarding payments on our channels. The total fee charged is basefee + (amount * feerate / 1000000), where amount is the forwarded amount."` + TimeLockDelta uint32 `long:"timelockdelta" description:"The CLTV delta we will subtract from a forwarded HTLC's timelock value"` } type neutrinoConfig struct { @@ -146,8 +147,6 @@ type config struct { Litecoin *chainConfig `group:"Litecoin" namespace:"litecoin"` Bitcoin *chainConfig `group:"Bitcoin" namespace:"bitcoin"` - DefaultNumChanConfs int `long:"defaultchanconfs" description:"The default number of confirmations a channel must have before it's considered open."` - NeutrinoMode *neutrinoConfig `group:"neutrino" namespace:"neutrino"` Autopilot *autoPilotConfig `group:"autopilot" namespace:"autopilot"` @@ -169,20 +168,19 @@ type config struct { // 4) Parse CLI options and overwrite/add any specified options func loadConfig() (*config, error) { defaultCfg := config{ - ConfigFile: defaultConfigFile, - DataDir: defaultDataDir, - DebugLevel: defaultLogLevel, - TLSCertPath: defaultTLSCertPath, - TLSKeyPath: defaultTLSKeyPath, - AdminMacPath: defaultAdminMacPath, - ReadMacPath: defaultReadMacPath, - LogDir: defaultLogDir, - PeerPort: defaultPeerPort, - RPCPort: defaultRPCPort, - RESTPort: defaultRESTPort, - MaxPendingChannels: defaultMaxPendingChannels, - DefaultNumChanConfs: defaultNumChanConfs, - NoEncryptWallet: defaultNoEncryptWallet, + ConfigFile: defaultConfigFile, + DataDir: defaultDataDir, + DebugLevel: defaultLogLevel, + TLSCertPath: defaultTLSCertPath, + TLSKeyPath: defaultTLSKeyPath, + AdminMacPath: defaultAdminMacPath, + ReadMacPath: defaultReadMacPath, + LogDir: defaultLogDir, + PeerPort: defaultPeerPort, + RPCPort: defaultRPCPort, + RESTPort: defaultRESTPort, + MaxPendingChannels: defaultMaxPendingChannels, + NoEncryptWallet: defaultNoEncryptWallet, Bitcoin: &chainConfig{ RPCHost: defaultRPCHost, RPCCert: defaultBtcdRPCCertFile, From 4ee3bff771b3b228a05585a73661f0882c22b82e Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 20 Dec 2017 16:17:51 +0100 Subject: [PATCH 22/39] lnd: use cfg.Bitcoin.DefaultNumChanConfs --- lnd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnd.go b/lnd.go index 12fc9edf..19a94b63 100644 --- a/lnd.go +++ b/lnd.go @@ -302,7 +302,7 @@ func lndMain() error { // TODO(roasbeef): add configurable mapping // * simple switch initially // * assign coefficient, etc - return uint16(cfg.DefaultNumChanConfs) + return uint16(cfg.Bitcoin.DefaultNumChanConfs) }, RequiredRemoteDelay: func(chanAmt btcutil.Amount) uint16 { // TODO(roasbeef): add additional hooks for From 53b0ee3765d036642027732d1fc3c10da4225295 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 19 Dec 2017 17:07:35 +0100 Subject: [PATCH 23/39] lntest/node: use --bitcoin.defaultchanconfs=1 and --bitcoin.defaultremotedelay=4 --- lntest/node.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lntest/node.go b/lntest/node.go index 1e2fd7b7..43f93292 100644 --- a/lntest/node.go +++ b/lntest/node.go @@ -122,7 +122,8 @@ func (cfg nodeConfig) genArgs() []string { args = append(args, "--nobootstrap") args = append(args, "--noencryptwallet") args = append(args, "--debuglevel=debug") - args = append(args, "--defaultchanconfs=1") + args = append(args, "--bitcoin.defaultchanconfs=1") + args = append(args, "--bitcoin.defaultremotedelay=4") args = append(args, fmt.Sprintf("--bitcoin.rpchost=%v", cfg.RPCConfig.Host)) args = append(args, fmt.Sprintf("--bitcoin.rpcuser=%v", cfg.RPCConfig.User)) args = append(args, fmt.Sprintf("--bitcoin.rpcpass=%v", cfg.RPCConfig.Pass)) From 3edc1a745611d1643e8915688bbe9cccde94d93b Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sun, 17 Dec 2017 16:08:21 +0100 Subject: [PATCH 24/39] fundingmanager: define min and maxRemoteDelay This commit defines minRemoteDelay and maxRemoteDelay, which is the extremes of the CSV delay we will require the remote to use for its commitment transaction. The actual delay we will require will be somewhere between these values, depending on channel size. --- fundingmanager.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fundingmanager.go b/fundingmanager.go index 75ba76d6..65f411c2 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -32,8 +32,6 @@ const ( // TODO(roasbeef): tune msgBufferSize = 50 - defaultCsvDelay = 4 - // maxFundingAmount is a soft-limit of the maximum channel size // accepted within the Lightning Protocol Currently. This limit is // currently defined in BOLT-0002, and serves as an initial @@ -43,6 +41,13 @@ const ( // TODO(roasbeef): add command line param to modify maxFundingAmount = btcutil.Amount(1 << 24) + // minRemoteDelay and maxRemoteDelay is the extremes of the CSV delay + // we will require the remote to use for its commitment transaction. + // The actual delay we will require will be somewhere between these + // values, depending on channel size. + minRemoteDelay = 144 + maxRemoteDelay = 2016 + // maxWaitNumBlocksFundingConf is the maximum number of blocks to wait // for the funding transaction to be confirmed before forgetting about // the channel. 288 blocks is ~48 hrs From 43d7dd3d99b7cd9b889e8baa02f2d47670bff379 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sun, 17 Dec 2017 16:13:23 +0100 Subject: [PATCH 25/39] lnd: make NumRequiredConfs and RequiredRemoteDelay scale with chanAmt This commit makes the value returned fomr NumRequiredConfs and RequiredRemoteDelay used during the funding process scale linearly with the channel size. This is done to ensure that in cases there are more at stake in a channel, we have more time to react to reorgs, or unilateral closes. If the user explicitly specified values for these two at startup, we return those instead, without doing the scaling. --- lnd.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/lnd.go b/lnd.go index 19a94b63..af9e0bb4 100644 --- a/lnd.go +++ b/lnd.go @@ -298,16 +298,71 @@ func lndMain() error { return nil, fmt.Errorf("unable to find channel") }, DefaultRoutingPolicy: activeChainControl.routingPolicy, - NumRequiredConfs: func(chanAmt btcutil.Amount, pushAmt lnwire.MilliSatoshi) uint16 { - // TODO(roasbeef): add configurable mapping - // * simple switch initially - // * assign coefficient, etc - return uint16(cfg.Bitcoin.DefaultNumChanConfs) + NumRequiredConfs: func(chanAmt btcutil.Amount, + pushAmt lnwire.MilliSatoshi) uint16 { + // For large channels we increase the number + // of confirmations we require for the + // channel to be considered open. As it is + // always the responder that gets to choose + // value, the pushAmt is value being pushed + // to us. This means we have more to lose + // in the case this gets re-orged out, and + // we will require more confirmations before + // we consider it open. + // TODO(halseth): Use Litecoin params in case + // of LTC channels. + + // In case the user has explicitly specified + // a default value for the number of + // confirmations, we use it. + defaultConf := uint16(cfg.Bitcoin.DefaultNumChanConfs) + if defaultConf != 0 { + return defaultConf + } + + // If not we return a value scaled linearly + // between 3 and 6, depending on channel size. + // TODO(halseth): Use 1 as minimum? + minConf := uint64(3) + maxConf := uint64(6) + maxChannelSize := uint64( + lnwire.NewMSatFromSatoshis(maxFundingAmount)) + stake := lnwire.NewMSatFromSatoshis(chanAmt) + pushAmt + conf := maxConf * uint64(stake) / maxChannelSize + if conf < minConf { + conf = minConf + } + if conf > maxConf { + conf = maxConf + } + return uint16(conf) }, RequiredRemoteDelay: func(chanAmt btcutil.Amount) uint16 { - // TODO(roasbeef): add additional hooks for - // configuration - return 4 + // We scale the remote CSV delay (the time the + // remote have to claim funds in case of a unilateral + // close) linearly from minRemoteDelay blocks + // for small channels, to maxRemoteDelay blocks + // for channels of size maxFundingAmount. + // TODO(halseth): Litecoin parameter for LTC. + + // In case the user has explicitly specified + // a default value for the remote delay, we + // use it. + defaultDelay := uint16(cfg.Bitcoin.DefaultRemoteDelay) + if defaultDelay > 0 { + return defaultDelay + } + + // If not we scale according to channel size. + delay := uint16(maxRemoteDelay * + chanAmt / maxFundingAmount) + if delay < minRemoteDelay { + delay = minRemoteDelay + } + if delay > maxRemoteDelay { + delay = maxRemoteDelay + } + return delay }, }) if err != nil { From 483abbee5b0097528957424389854c6dd358c700 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 20 Dec 2017 14:02:04 +0100 Subject: [PATCH 26/39] discovery: make sure ChannelUpdates always have incremented timestamp This commit ensures that we always increment the timestamp of ChannelUpdates we send telling the network about changes to our channel policy. We do this because it could happen (especially during tests) that we issued an update, but the ChannelUpdate would have the same timestamp as our last ChannelUpdate, and would be ignored by the network. --- discovery/gossiper.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/discovery/gossiper.go b/discovery/gossiper.go index bf876257..4dedfae2 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -1986,12 +1986,18 @@ func (d *AuthenticatedGossiper) sendAnnSigReliably( func (d *AuthenticatedGossiper) updateChannel(info *channeldb.ChannelEdgeInfo, edge *channeldb.ChannelEdgePolicy) (*lnwire.ChannelAnnouncement, *lnwire.ChannelUpdate, error) { - edge.LastUpdate = time.Now() + // Make sure timestamp is always increased, such that our update + // gets propagated. + timestamp := time.Now().Unix() + if timestamp <= edge.LastUpdate.Unix() { + timestamp = edge.LastUpdate.Unix() + 1 + } + edge.LastUpdate = time.Unix(timestamp, 0) chanUpdate := &lnwire.ChannelUpdate{ Signature: edge.Signature, ChainHash: info.ChainHash, ShortChannelID: lnwire.NewShortChanIDFromInt(edge.ChannelID), - Timestamp: uint32(edge.LastUpdate.Unix()), + Timestamp: uint32(timestamp), Flags: edge.Flags, TimeLockDelta: edge.TimeLockDelta, HtlcMinimumMsat: edge.MinHTLC, From f4f024aff2a62e726a7c29939762504fc2244d5e Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 22 Dec 2017 13:49:49 +0100 Subject: [PATCH 27/39] lnd_test: extract graph topology subscription into own method This commit extracts the launching of a goroutine subscribing to and forwarding graph topology notifications into its own utility method, such that it can be used in other tests as well. --- lnd_test.go | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/lnd_test.go b/lnd_test.go index a9a0ab72..3cf28b4e 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -3878,28 +3878,22 @@ out: } } -func testGraphTopologyNotifications(net *lntest.NetworkHarness, t *harnessTest) { - const chanAmt = maxFundingAmount - timeout := time.Duration(time.Second * 5) - ctxb := context.Background() - - // We'll first start by establishing a notification client to Alice which - // will send us notifications upon detected changes in the channel graph. +// subscribeGraphNotifications subscribes to channel graph updates and launches +// a goroutine that forwards these to the returned channel. +func subscribeGraphNotifications(t *harnessTest, ctxb context.Context, + node *lntest.HarnessNode) (chan *lnrpc.GraphTopologyUpdate, chan struct{}) { + // We'll first start by establishing a notification client which will + // send us notifications upon detected changes in the channel graph. req := &lnrpc.GraphTopologySubscription{} - topologyClient, err := net.Alice.SubscribeChannelGraph(ctxb, req) + topologyClient, err := node.SubscribeChannelGraph(ctxb, req) if err != nil { t.Fatalf("unable to create topology client: %v", err) } - // Open a new channel between Alice and Bob. - ctxt, _ := context.WithTimeout(ctxb, timeout) - chanPoint := openChannelAndAssert(ctxt, t, net, net.Alice, net.Bob, - chanAmt, 0) - // We'll launch a goroutine that'll be responsible for proxying all // notifications recv'd from the client into the channel below. quit := make(chan struct{}) - graphUpdates := make(chan *lnrpc.GraphTopologyUpdate, 4) + graphUpdates := make(chan *lnrpc.GraphTopologyUpdate, 20) go func() { for { select { @@ -3916,7 +3910,8 @@ func testGraphTopologyNotifications(net *lntest.NetworkHarness, t *harnessTest) if err == io.EOF { return } else if err != nil { - t.Fatalf("unable to recv graph update: %v", err) + t.Fatalf("unable to recv graph update: %v", + err) } select { @@ -3927,6 +3922,21 @@ func testGraphTopologyNotifications(net *lntest.NetworkHarness, t *harnessTest) } } }() + return graphUpdates, quit +} + +func testGraphTopologyNotifications(net *lntest.NetworkHarness, t *harnessTest) { + const chanAmt = maxFundingAmount + timeout := time.Duration(time.Second * 5) + ctxb := context.Background() + + // Let Alice subscribe to graph notifications. + graphUpdates, quit := subscribeGraphNotifications(t, ctxb, net.Alice) + + // Open a new channel between Alice and Bob. + ctxt, _ := context.WithTimeout(ctxb, timeout) + chanPoint := openChannelAndAssert(ctxt, t, net, net.Alice, net.Bob, + chanAmt, 0) // The channel opening above should've triggered a few notifications // sent to the notification client. We'll expect two channel updates, From d030773c8d2cede68bd2d265be2a5053f4b1b165 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 19 Dec 2017 17:10:00 +0100 Subject: [PATCH 28/39] lnd test: add testUpdateChannelPolicy This commit adds a new integration test, that checks that policy/fee updates get propagated properly in the network, such that the other nodes learn about the changes. --- lnd_test.go | 290 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 289 insertions(+), 1 deletion(-) diff --git a/lnd_test.go b/lnd_test.go index 3cf28b4e..b87aced0 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -467,6 +467,290 @@ func testBasicChannelFunding(net *lntest.NetworkHarness, t *harnessTest) { closeChannelAndAssert(ctxt, t, net, net.Alice, chanPoint, false) } +// testUpdateChannelPolicy tests that policy updates made to a channel +// gets propagated to other nodes in the network. +func testUpdateChannelPolicy(net *lntest.NetworkHarness, t *harnessTest) { + timeout := time.Duration(time.Second * 5) + ctxb := context.Background() + + // Launch notification clients for all nodes, such that we can + // get notified when they discover new channels and updates + // in the graph. + aliceUpdates, aQuit := subscribeGraphNotifications(t, ctxb, net.Alice) + defer close(aQuit) + bobUpdates, bQuit := subscribeGraphNotifications(t, ctxb, net.Bob) + defer close(bQuit) + + chanAmt := maxFundingAmount + pushAmt := btcutil.Amount(100000) + + // Create a channel Alice->Bob. + ctxt, _ := context.WithTimeout(ctxb, timeout) + chanPoint := openChannelAndAssert(ctxt, t, net, net.Alice, net.Bob, + chanAmt, pushAmt) + + ctxt, _ = context.WithTimeout(ctxb, time.Second*15) + err := net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint) + if err != nil { + t.Fatalf("alice didn't report channel: %v", err) + } + err = net.Bob.WaitForNetworkChannelOpen(ctxt, chanPoint) + if err != nil { + t.Fatalf("bob didn't report channel: %v", err) + } + + // Create Carol and a new channel Bob->Carol. + carol, err := net.NewNode(nil) + if err != nil { + t.Fatalf("unable to create new nodes: %v", err) + } + carolUpdates, cQuit := subscribeGraphNotifications(t, ctxb, carol) + defer close(cQuit) + + if err := net.ConnectNodes(ctxb, carol, net.Bob); err != nil { + t.Fatalf("unable to connect dave to alice: %v", err) + } + + ctxt, _ = context.WithTimeout(ctxb, timeout) + chanPoint2 := openChannelAndAssert(ctxt, t, net, net.Bob, carol, + chanAmt, pushAmt) + + ctxt, _ = context.WithTimeout(ctxb, time.Second*15) + err = net.Bob.WaitForNetworkChannelOpen(ctxt, chanPoint2) + if err != nil { + t.Fatalf("bob didn't report channel: %v", err) + } + err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint2) + if err != nil { + t.Fatalf("carol didn't report channel: %v", err) + } + + // Update the fees for the channel Alice->Bob, and make sure + // all nodes learn about it. + const feeBase = 1000000 + baseFee := int64(1500) + feeRate := int64(12) + timeLockDelta := uint32(66) + + req := &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: baseFee, + FeeRate: float64(feeRate), + TimeLockDelta: timeLockDelta, + } + req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: chanPoint, + } + + _, err = net.Alice.UpdateChannelPolicy(ctxb, req) + if err != nil { + t.Fatalf("unable to get alice's balance: %v", err) + } + + // txStr returns the string representation of the channel's + // funding tx. + txStr := func(chanPoint *lnrpc.ChannelPoint) string { + fundingTxID, err := chainhash.NewHash(chanPoint.FundingTxid) + if err != nil { + return "" + } + cp := wire.OutPoint{ + Hash: *fundingTxID, + Index: chanPoint.OutputIndex, + } + return cp.String() + } + + // A closure that is used to wait for a channel updates that matches + // the channel policy update done by Alice. + waitForChannelUpdate := func(graphUpdates chan *lnrpc.GraphTopologyUpdate, + chanPoints ...*lnrpc.ChannelPoint) { + // Create a map containing all the channel points we are + // waiting for updates for. + cps := make(map[string]bool) + for _, chanPoint := range chanPoints { + cps[txStr(chanPoint)] = true + } + Loop: + for { + select { + case graphUpdate := <-graphUpdates: + if len(graphUpdate.ChannelUpdates) == 0 { + continue + } + chanUpdate := graphUpdate.ChannelUpdates[0] + fundingTxStr := txStr(chanUpdate.ChanPoint) + if _, ok := cps[fundingTxStr]; !ok { + continue + } + + if chanUpdate.AdvertisingNode != net.Alice.PubKeyStr { + continue + } + + policy := chanUpdate.RoutingPolicy + if policy.FeeBaseMsat != baseFee { + continue + } + if policy.FeeRateMilliMsat != feeRate*feeBase { + continue + } + if policy.TimeLockDelta != timeLockDelta { + continue + } + + // We got a policy update that matched the + // values and channel point of what we + // expected, delete it from the map. + delete(cps, fundingTxStr) + + // If we have no more channel points we are + // waiting for, break out of the loop. + if len(cps) == 0 { + break Loop + } + case <-time.After(20 * time.Second): + t.Fatalf("did not receive channel update") + } + } + } + + // Wait for all nodes to have seen the policy update done by Alice. + waitForChannelUpdate(aliceUpdates, chanPoint) + waitForChannelUpdate(bobUpdates, chanPoint) + waitForChannelUpdate(carolUpdates, chanPoint) + + // assertChannelPolicy asserts that the passed node's known channel + // policy for the passed chanPoint is consistent with Alice's current + // expected policy values. + assertChannelPolicy := func(node *lntest.HarnessNode, + chanPoint *lnrpc.ChannelPoint) { + // Get a DescribeGraph from the node. + descReq := &lnrpc.ChannelGraphRequest{} + chanGraph, err := node.DescribeGraph(ctxb, descReq) + if err != nil { + t.Fatalf("unable to query for alice's routing table: %v", + err) + } + + edgeFound := false + for _, e := range chanGraph.Edges { + if e.ChanPoint == txStr(chanPoint) { + edgeFound = true + if e.Node1Pub == net.Alice.PubKeyStr { + if e.Node1Policy.FeeBaseMsat != baseFee { + t.Fatalf("expected base fee "+ + "%v, got %v", baseFee, + e.Node1Policy.FeeBaseMsat) + } + if e.Node1Policy.FeeRateMilliMsat != feeRate*feeBase { + t.Fatalf("expected fee rate "+ + "%v, got %v", feeRate*feeBase, + e.Node1Policy.FeeRateMilliMsat) + } + if e.Node1Policy.TimeLockDelta != timeLockDelta { + t.Fatalf("expected time lock "+ + "delta %v, got %v", + timeLockDelta, + e.Node1Policy.TimeLockDelta) + } + } else { + if e.Node2Policy.FeeBaseMsat != baseFee { + t.Fatalf("expected base fee "+ + "%v, got %v", baseFee, + e.Node2Policy.FeeBaseMsat) + } + if e.Node2Policy.FeeRateMilliMsat != feeRate*feeBase { + t.Fatalf("expected fee rate "+ + "%v, got %v", feeRate*feeBase, + e.Node2Policy.FeeRateMilliMsat) + } + if e.Node2Policy.TimeLockDelta != timeLockDelta { + t.Fatalf("expected time lock "+ + "delta %v, got %v", + timeLockDelta, + e.Node2Policy.TimeLockDelta) + } + } + } + } + + if !edgeFound { + t.Fatalf("did not find edge") + } + + } + + // Check that all nodes now know about Alice's updated policy. + assertChannelPolicy(net.Alice, chanPoint) + assertChannelPolicy(net.Bob, chanPoint) + assertChannelPolicy(carol, chanPoint) + + // Open channel to Carol. + if err := net.ConnectNodes(ctxb, net.Alice, carol); err != nil { + t.Fatalf("unable to connect dave to alice: %v", err) + } + ctxt, _ = context.WithTimeout(ctxb, timeout) + chanPoint3 := openChannelAndAssert(ctxt, t, net, net.Alice, carol, + chanAmt, pushAmt) + + ctxt, _ = context.WithTimeout(ctxb, time.Second*15) + err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint3) + if err != nil { + t.Fatalf("alice didn't report channel: %v", err) + } + err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint3) + if err != nil { + t.Fatalf("bob didn't report channel: %v", err) + } + + // Make a global update, and check that both channels' + // new policies get propagated. + baseFee = int64(800) + feeRate = int64(123) + timeLockDelta = uint32(22) + + req = &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: baseFee, + FeeRate: float64(feeRate), + TimeLockDelta: timeLockDelta, + } + req.Scope = &lnrpc.PolicyUpdateRequest_Global{} + + _, err = net.Alice.UpdateChannelPolicy(ctxb, req) + if err != nil { + t.Fatalf("unable to get alice's balance: %v", err) + } + + // Wait for all nodes to have seen the policy updates + // for both of Alice's channels. + waitForChannelUpdate(aliceUpdates, chanPoint, chanPoint3) + waitForChannelUpdate(bobUpdates, chanPoint, chanPoint3) + waitForChannelUpdate(carolUpdates, chanPoint, chanPoint3) + + // And finally check that all nodes remembers the policy + // update they received. + assertChannelPolicy(net.Alice, chanPoint) + assertChannelPolicy(net.Bob, chanPoint) + assertChannelPolicy(carol, chanPoint) + + assertChannelPolicy(net.Alice, chanPoint3) + assertChannelPolicy(net.Bob, chanPoint3) + assertChannelPolicy(carol, chanPoint3) + + // Close the channels. + ctxt, _ = context.WithTimeout(ctxb, timeout) + closeChannelAndAssert(ctxt, t, net, net.Alice, chanPoint, false) + ctxt, _ = context.WithTimeout(ctxb, timeout) + closeChannelAndAssert(ctxt, t, net, net.Bob, chanPoint2, false) + closeChannelAndAssert(ctxt, t, net, net.Alice, chanPoint3, false) + ctxt, _ = context.WithTimeout(ctxb, timeout) + + // Clean up carol's node. + if err := net.ShutdownNode(carol); err != nil { + t.Fatalf("unable to shutdown carol: %v", err) + } +} + // testOpenChannelAfterReorg tests that in the case where we have an open // channel where the funding tx gets reorged out, the channel will no // longer be present in the node's routing table. @@ -793,7 +1077,7 @@ func testChannelFundingPersistence(net *lntest.NetworkHarness, t *harnessTest) { // confirmation before it's open, with the current set of defaults, // we'll need to create a new node instance. const numConfs = 5 - carolArgs := []string{fmt.Sprintf("--defaultchanconfs=%v", numConfs)} + carolArgs := []string{fmt.Sprintf("--bitcoin.defaultchanconfs=%v", numConfs)} carol, err := net.NewNode(carolArgs) if err != nil { t.Fatalf("unable to create new node: %v", err) @@ -4690,6 +4974,10 @@ var testsCases = []*testCase{ name: "basic funding flow", test: testBasicChannelFunding, }, + { + name: "update channel policy", + test: testUpdateChannelPolicy, + }, { name: "open channel reorg test", test: testOpenChannelAfterReorg, From 9ab2ab0424fd9d0b79dfdedb651909af150613e7 Mon Sep 17 00:00:00 2001 From: Timo Schmid Date: Sat, 13 Jan 2018 21:27:06 +0100 Subject: [PATCH 29/39] rpc: remove SetAlias from the rpc docs --- lnrpc/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/lnrpc/README.md b/lnrpc/README.md index 6d2f702f..6d7e63b4 100644 --- a/lnrpc/README.md +++ b/lnrpc/README.md @@ -89,8 +89,6 @@ description): amount of payment. * GetNetworkInfo * Returns some network level statistics. - * SetAlias - * Sets the node alias which is to be advertised on the network. ## Installation and Updating From 187f59556a19ecf61df12d0b4b9d3e42e3131428 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 9 Nov 2017 17:30:20 -0700 Subject: [PATCH 30/39] multi: add bitcoind drivers and tests --- chainntnfs/bitcoindnotify/bitcoind.go | 679 ++++++++++++++++++++++++++ chainntnfs/bitcoindnotify/driver.go | 53 ++ chainntnfs/interface_test.go | 188 ++++++- glide.lock | 6 +- glide.yaml | 1 + lnwallet/btcwallet/blockchain.go | 84 +--- lnwallet/btcwallet/btcwallet.go | 48 +- lnwallet/btcwallet/driver.go | 2 + lnwallet/fee_estimator.go | 144 ++++++ lnwallet/interface.go | 9 + lnwallet/interface_test.go | 567 +++++++++++++++------ routing/chainview/bitcoind.go | 460 +++++++++++++++++ routing/chainview/interface_test.go | 138 +++++- 13 files changed, 2138 insertions(+), 241 deletions(-) create mode 100644 chainntnfs/bitcoindnotify/bitcoind.go create mode 100644 chainntnfs/bitcoindnotify/driver.go create mode 100644 routing/chainview/bitcoind.go diff --git a/chainntnfs/bitcoindnotify/bitcoind.go b/chainntnfs/bitcoindnotify/bitcoind.go new file mode 100644 index 00000000..8be208b6 --- /dev/null +++ b/chainntnfs/bitcoindnotify/bitcoind.go @@ -0,0 +1,679 @@ +package bitcoindnotify + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/roasbeef/btcd/btcjson" + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcd/chaincfg/chainhash" + "github.com/roasbeef/btcd/rpcclient" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" + "github.com/roasbeef/btcwallet/chain" +) + +const ( + + // notifierType uniquely identifies this concrete implementation of the + // ChainNotifier interface. + notifierType = "bitcoind" + + // reorgSafetyLimit is assumed maximum depth of a chain reorganization. + // After this many confirmation, transaction confirmation info will be + // pruned. + reorgSafetyLimit = 100 +) + +var ( + // ErrChainNotifierShuttingDown is used when we are trying to + // measure a spend notification when notifier is already stopped. + ErrChainNotifierShuttingDown = errors.New("chainntnfs: system interrupt " + + "while attempting to register for spend notification.") +) + +// chainUpdate encapsulates an update to the current main chain. This struct is +// used as an element within an unbounded queue in order to avoid blocking the +// main rpc dispatch rule. +type chainUpdate struct { + blockHash *chainhash.Hash + blockHeight int32 +} + +// txUpdate encapsulates a transaction related notification sent from bitcoind +// to the registered RPC client. This struct is used as an element within an +// unbounded queue in order to avoid blocking the main rpc dispatch rule. +type txUpdate struct { + tx *btcutil.Tx + details *btcjson.BlockDetails +} + +// TODO(roasbeef): generalize struct below: +// * move chans to config, allow outside callers to handle send conditions + +// BitcoindNotifier implements the ChainNotifier interface using a bitcoind +// chain client. Multiple concurrent clients are supported. All notifications +// are achieved via non-blocking sends on client channels. +type BitcoindNotifier struct { + spendClientCounter uint64 // To be used atomically. + epochClientCounter uint64 // To be used atomically. + + started int32 // To be used atomically. + stopped int32 // To be used atomically. + + heightMtx sync.RWMutex + bestHeight int32 + + chainConn *chain.BitcoindClient + + notificationCancels chan interface{} + notificationRegistry chan interface{} + + spendNotifications map[wire.OutPoint]map[uint64]*spendNotification + + txConfNotifier *chainntnfs.TxConfNotifier + + blockEpochClients map[uint64]*blockEpochRegistration + + wg sync.WaitGroup + quit chan struct{} +} + +// Ensure BitcoindNotifier implements the ChainNotifier interface at compile +// time. +var _ chainntnfs.ChainNotifier = (*BitcoindNotifier)(nil) + +// New returns a new BitcoindNotifier instance. This function assumes the +// bitcoind node detailed in the passed configuration is already running, and +// willing to accept RPC requests and new zmq clients. +func New(config *rpcclient.ConnConfig, zmqConnect string, + params chaincfg.Params) (*BitcoindNotifier, error) { + notifier := &BitcoindNotifier{ + notificationCancels: make(chan interface{}), + notificationRegistry: make(chan interface{}), + + blockEpochClients: make(map[uint64]*blockEpochRegistration), + + spendNotifications: make(map[wire.OutPoint]map[uint64]*spendNotification), + + quit: make(chan struct{}), + } + + // Disable connecting to bitcoind within the rpcclient.New method. We + // defer establishing the connection to our .Start() method. + config.DisableConnectOnNew = true + config.DisableAutoReconnect = false + chainConn, err := chain.NewBitcoindClient(¶ms, config.Host, + config.User, config.Pass, zmqConnect, 100*time.Millisecond) + if err != nil { + return nil, err + } + notifier.chainConn = chainConn + + return notifier, nil +} + +// Start connects to the running bitcoind node over websockets, registers for +// block notifications, and finally launches all related helper goroutines. +func (b *BitcoindNotifier) Start() error { + // Already started? + if atomic.AddInt32(&b.started, 1) != 1 { + return nil + } + + // Connect to bitcoind, and register for notifications on connected, + // and disconnected blocks. + if err := b.chainConn.Start(); err != nil { + return err + } + if err := b.chainConn.NotifyBlocks(); err != nil { + return err + } + + _, currentHeight, err := b.chainConn.GetBestBlock() + if err != nil { + return err + } + + b.heightMtx.Lock() + b.bestHeight = currentHeight + b.heightMtx.Unlock() + + b.txConfNotifier = chainntnfs.NewTxConfNotifier( + uint32(currentHeight), reorgSafetyLimit) + + b.wg.Add(1) + go b.notificationDispatcher() + + return nil +} + +// Stop shutsdown the BitcoindNotifier. +func (b *BitcoindNotifier) Stop() error { + // Already shutting down? + if atomic.AddInt32(&b.stopped, 1) != 1 { + return nil + } + + // Shutdown the rpc client, this gracefully disconnects from bitcoind, + // and cleans up all related resources. + b.chainConn.Stop() + + close(b.quit) + b.wg.Wait() + + // Notify all pending clients of our shutdown by closing the related + // notification channels. + for _, spendClients := range b.spendNotifications { + for _, spendClient := range spendClients { + close(spendClient.spendChan) + } + } + for _, epochClient := range b.blockEpochClients { + close(epochClient.epochChan) + } + b.txConfNotifier.TearDown() + + return nil +} + +// blockNtfn packages a notification of a connected/disconnected block along +// with its height at the time. +type blockNtfn struct { + sha *chainhash.Hash + height int32 +} + +// notificationDispatcher is the primary goroutine which handles client +// notification registrations, as well as notification dispatches. +func (b *BitcoindNotifier) notificationDispatcher() { +out: + for { + select { + case cancelMsg := <-b.notificationCancels: + switch msg := cancelMsg.(type) { + case *spendCancel: + chainntnfs.Log.Infof("Cancelling spend "+ + "notification for out_point=%v, "+ + "spend_id=%v", msg.op, msg.spendID) + + // Before we attempt to close the spendChan, + // ensure that the notification hasn't already + // yet been dispatched. + if outPointClients, ok := b.spendNotifications[msg.op]; ok { + close(outPointClients[msg.spendID].spendChan) + delete(b.spendNotifications[msg.op], msg.spendID) + } + + case *epochCancel: + chainntnfs.Log.Infof("Cancelling epoch "+ + "notification, epoch_id=%v", msg.epochID) + + // First, close the cancel channel for this + // specific client, and wait for the client to + // exit. + close(b.blockEpochClients[msg.epochID].cancelChan) + b.blockEpochClients[msg.epochID].wg.Wait() + + // Once the client has exited, we can then + // safely close the channel used to send epoch + // notifications, in order to notify any + // listeners that the intent has been + // cancelled. + close(b.blockEpochClients[msg.epochID].epochChan) + delete(b.blockEpochClients, msg.epochID) + + } + case registerMsg := <-b.notificationRegistry: + switch msg := registerMsg.(type) { + case *spendNotification: + chainntnfs.Log.Infof("New spend subscription: "+ + "utxo=%v", msg.targetOutpoint) + op := *msg.targetOutpoint + + if _, ok := b.spendNotifications[op]; !ok { + b.spendNotifications[op] = make(map[uint64]*spendNotification) + } + b.spendNotifications[op][msg.spendID] = msg + b.chainConn.NotifySpent([]*wire.OutPoint{&op}) + case *confirmationsNotification: + chainntnfs.Log.Infof("New confirmations "+ + "subscription: txid=%v, numconfs=%v", + msg.TxID, msg.NumConfirmations) + + // Lookup whether the transaction is already included in the + // active chain. + txConf, err := b.historicalConfDetails(msg.TxID) + if err != nil { + chainntnfs.Log.Error(err) + } + b.heightMtx.RLock() + err = b.txConfNotifier.Register(&msg.ConfNtfn, txConf) + if err != nil { + chainntnfs.Log.Error(err) + } + b.heightMtx.RUnlock() + case *blockEpochRegistration: + chainntnfs.Log.Infof("New block epoch subscription") + b.blockEpochClients[msg.epochID] = msg + } + + case ntfn := <-b.chainConn.Notifications(): + switch item := ntfn.(type) { + case chain.BlockConnected: + b.heightMtx.Lock() + if item.Height != b.bestHeight+1 { + chainntnfs.Log.Warnf("Received blocks out of order: "+ + "current height=%d, new height=%d", + b.bestHeight, item.Height) + b.heightMtx.Unlock() + continue + } + b.bestHeight = item.Height + + rawBlock, err := b.chainConn.GetBlock(&item.Hash) + if err != nil { + chainntnfs.Log.Errorf("Unable to get block: %v", err) + b.heightMtx.Unlock() + continue + } + + chainntnfs.Log.Infof("New block: height=%v, sha=%v", + item.Height, item.Hash) + + b.notifyBlockEpochs(item.Height, &item.Hash) + + txns := btcutil.NewBlock(rawBlock).Transactions() + err = b.txConfNotifier.ConnectTip(&item.Hash, + uint32(item.Height), txns) + if err != nil { + chainntnfs.Log.Error(err) + } + b.heightMtx.Unlock() + continue + + case chain.BlockDisconnected: + b.heightMtx.Lock() + if item.Height != b.bestHeight { + chainntnfs.Log.Warnf("Received blocks "+ + "out of order: current height="+ + "%d, disconnected height=%d", + b.bestHeight, item.Height) + b.heightMtx.Unlock() + continue + } + b.bestHeight = item.Height - 1 + + chainntnfs.Log.Infof("Block disconnected from "+ + "main chain: height=%v, sha=%v", + item.Height, item.Hash) + + err := b.txConfNotifier.DisconnectTip( + uint32(item.Height)) + if err != nil { + chainntnfs.Log.Error(err) + } + b.heightMtx.Unlock() + + case chain.RelevantTx: + tx := item.TxRecord.MsgTx + // First, check if this transaction spends an output + // that has an existing spend notification for it. + for i, txIn := range tx.TxIn { + prevOut := txIn.PreviousOutPoint + + // If this transaction indeed does spend an + // output which we have a registered + // notification for, then create a spend + // summary, finally sending off the details to + // the notification subscriber. + if clients, ok := b.spendNotifications[prevOut]; ok { + spenderSha := tx.TxHash() + spendDetails := &chainntnfs.SpendDetail{ + SpentOutPoint: &prevOut, + SpenderTxHash: &spenderSha, + SpendingTx: &tx, + SpenderInputIndex: uint32(i), + } + // TODO(roasbeef): after change to + // loadfilter, only notify on block + // inclusion? + if item.Block != nil { + spendDetails.SpendingHeight = item.Block.Height + } else { + b.heightMtx.RLock() + spendDetails.SpendingHeight = b.bestHeight + 1 + b.heightMtx.RUnlock() + } + + for _, ntfn := range clients { + chainntnfs.Log.Infof("Dispatching "+ + "spend notification for "+ + "outpoint=%v", ntfn.targetOutpoint) + ntfn.spendChan <- spendDetails + + // Close spendChan to ensure that any calls to Cancel will not + // block. This is safe to do since the channel is buffered, and the + // message can still be read by the receiver. + close(ntfn.spendChan) + } + delete(b.spendNotifications, prevOut) + } + } + } + + case <-b.quit: + break out + } + } + b.wg.Done() +} + +// historicalConfDetails looks up whether a transaction is already included in a +// block in the active chain and, if so, returns details about the confirmation. +func (b *BitcoindNotifier) historicalConfDetails(txid *chainhash.Hash, +) (*chainntnfs.TxConfirmation, error) { + + // If the transaction already has some or all of the confirmations, + // then we may be able to dispatch it immediately. + // TODO: fall back to scanning blocks if txindex isn't on. + tx, err := b.chainConn.GetRawTransactionVerbose(txid) + if err != nil || tx == nil || tx.BlockHash == "" { + if err == nil { + return nil, nil + } + // Do not return an error if the transaction was not found. + if jsonErr, ok := err.(*btcjson.RPCError); ok { + if jsonErr.Code == btcjson.ErrRPCNoTxInfo { + return nil, nil + } + } + return nil, fmt.Errorf("unable to query for txid(%v): %v", txid, err) + } + + // As we need to fully populate the returned TxConfirmation struct, + // grab the block in which the transaction was confirmed so we can + // locate its exact index within the block. + blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) + if err != nil { + return nil, fmt.Errorf("unable to get block hash %v for historical "+ + "dispatch: %v", tx.BlockHash, err) + } + block, err := b.chainConn.GetBlockVerbose(blockHash) + if err != nil { + return nil, fmt.Errorf("unable to get block hash: %v", err) + } + + // If the block obtained, locate the transaction's index within the + // block so we can give the subscriber full confirmation details. + txIndex := -1 + targetTxidStr := txid.String() + for i, txHash := range block.Tx { + if txHash == targetTxidStr { + txIndex = i + break + } + } + + if txIndex == -1 { + return nil, fmt.Errorf("unable to locate tx %v in block %v", + txid, blockHash) + } + + txConf := chainntnfs.TxConfirmation{ + BlockHash: blockHash, + BlockHeight: uint32(block.Height), + TxIndex: uint32(txIndex), + } + return &txConf, nil +} + +// notifyBlockEpochs notifies all registered block epoch clients of the newly +// connected block to the main chain. +func (b *BitcoindNotifier) notifyBlockEpochs(newHeight int32, newSha *chainhash.Hash) { + epoch := &chainntnfs.BlockEpoch{ + Height: newHeight, + Hash: newSha, + } + + for _, epochClient := range b.blockEpochClients { + b.wg.Add(1) + epochClient.wg.Add(1) + go func(ntfnChan chan *chainntnfs.BlockEpoch, cancelChan chan struct{}, + clientWg *sync.WaitGroup) { + + // TODO(roasbeef): move to goroutine per client, use sync queue + + defer clientWg.Done() + defer b.wg.Done() + + select { + case ntfnChan <- epoch: + + case <-cancelChan: + return + + case <-b.quit: + return + } + + }(epochClient.epochChan, epochClient.cancelChan, &epochClient.wg) + } +} + +// spendNotification couples a target outpoint along with the channel used for +// notifications once a spend of the outpoint has been detected. +type spendNotification struct { + targetOutpoint *wire.OutPoint + + spendChan chan *chainntnfs.SpendDetail + + spendID uint64 +} + +// spendCancel is a message sent to the BitcoindNotifier when a client wishes +// to cancel an outstanding spend notification that has yet to be dispatched. +type spendCancel struct { + // op is the target outpoint of the notification to be cancelled. + op wire.OutPoint + + // spendID the ID of the notification to cancel. + spendID uint64 +} + +// RegisterSpendNtfn registers an intent to be notified once the target +// outpoint has been spent by a transaction on-chain. Once a spend of the target +// outpoint has been detected, the details of the spending event will be sent +// across the 'Spend' channel. +func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, + _ uint32) (*chainntnfs.SpendEvent, error) { + + if err := b.chainConn.NotifySpent([]*wire.OutPoint{outpoint}); err != nil { + return nil, err + } + + ntfn := &spendNotification{ + targetOutpoint: outpoint, + spendChan: make(chan *chainntnfs.SpendDetail, 1), + spendID: atomic.AddUint64(&b.spendClientCounter, 1), + } + + select { + case <-b.quit: + return nil, ErrChainNotifierShuttingDown + case b.notificationRegistry <- ntfn: + } + + // The following conditional checks to ensure that when a spend notification + // is registered, the output hasn't already been spent. If the output + // is no longer in the UTXO set, the chain will be rescanned from the point + // where the output was added. The rescan will dispatch the notification. + txout, err := b.chainConn.GetTxOut(&outpoint.Hash, outpoint.Index, true) + if err != nil { + return nil, err + } + + if txout == nil { + // TODO: fall back to scanning blocks if txindex isn't on. + transaction, err := b.chainConn.GetRawTransactionVerbose(&outpoint.Hash) + if err != nil { + jsonErr, ok := err.(*btcjson.RPCError) + if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo { + return nil, err + } + } + + if transaction != nil { + blockhash, err := chainhash.NewHashFromStr(transaction.BlockHash) + if err != nil { + return nil, err + } + + // Rewind the rescan, since the btcwallet bitcoind + // back-end doesn't support that. + blockHeight, err := b.chainConn.GetBlockHeight(blockhash) + if err != nil { + return nil, err + } + b.heightMtx.Lock() + currentHeight := b.bestHeight + b.bestHeight = blockHeight + for i := currentHeight; i > blockHeight; i-- { + err = b.txConfNotifier.DisconnectTip(uint32(i)) + if err != nil { + return nil, err + } + } + b.heightMtx.Unlock() + + ops := []*wire.OutPoint{outpoint} + if err := b.chainConn.Rescan(blockhash, nil, ops); err != nil { + chainntnfs.Log.Errorf("Rescan for spend "+ + "notification txout failed: %v", err) + return nil, err + } + } + } + + return &chainntnfs.SpendEvent{ + Spend: ntfn.spendChan, + Cancel: func() { + cancel := &spendCancel{ + op: *outpoint, + spendID: ntfn.spendID, + } + + // Submit spend cancellation to notification dispatcher. + select { + case b.notificationCancels <- cancel: + // Cancellation is being handled, drain the spend chan until it is + // closed before yielding to the caller. + for { + select { + case _, ok := <-ntfn.spendChan: + if !ok { + return + } + case <-b.quit: + return + } + } + case <-b.quit: + } + }, + }, nil +} + +// confirmationNotification represents a client's intent to receive a +// notification once the target txid reaches numConfirmations confirmations. +type confirmationsNotification struct { + chainntnfs.ConfNtfn +} + +// RegisterConfirmationsNtfn registers a notification with BitcoindNotifier +// which will be triggered once the txid reaches numConfs number of +// confirmations. +func (b *BitcoindNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, + numConfs, _ uint32) (*chainntnfs.ConfirmationEvent, error) { + + ntfn := &confirmationsNotification{ + chainntnfs.ConfNtfn{ + TxID: txid, + NumConfirmations: numConfs, + Event: chainntnfs.NewConfirmationEvent(), + }, + } + + select { + case <-b.quit: + return nil, ErrChainNotifierShuttingDown + case b.notificationRegistry <- ntfn: + return ntfn.Event, nil + } +} + +// blockEpochRegistration represents a client's intent to receive a +// notification with each newly connected block. +type blockEpochRegistration struct { + epochID uint64 + + epochChan chan *chainntnfs.BlockEpoch + + cancelChan chan struct{} + + wg sync.WaitGroup +} + +// epochCancel is a message sent to the BitcoindNotifier when a client wishes +// to cancel an outstanding epoch notification that has yet to be dispatched. +type epochCancel struct { + epochID uint64 +} + +// RegisterBlockEpochNtfn returns a BlockEpochEvent which subscribes the +// caller to receive notifications, of each new block connected to the main +// chain. +func (b *BitcoindNotifier) RegisterBlockEpochNtfn() (*chainntnfs.BlockEpochEvent, error) { + registration := &blockEpochRegistration{ + epochChan: make(chan *chainntnfs.BlockEpoch, 20), + cancelChan: make(chan struct{}), + epochID: atomic.AddUint64(&b.epochClientCounter, 1), + } + + select { + case <-b.quit: + return nil, errors.New("chainntnfs: system interrupt while " + + "attempting to register for block epoch notification.") + case b.notificationRegistry <- registration: + return &chainntnfs.BlockEpochEvent{ + Epochs: registration.epochChan, + Cancel: func() { + cancel := &epochCancel{ + epochID: registration.epochID, + } + + // Submit epoch cancellation to notification dispatcher. + select { + case b.notificationCancels <- cancel: + // Cancellation is being handled, drain the epoch channel until it is + // closed before yielding to caller. + for { + select { + case _, ok := <-registration.epochChan: + if !ok { + return + } + case <-b.quit: + return + } + } + case <-b.quit: + } + }, + }, nil + } +} diff --git a/chainntnfs/bitcoindnotify/driver.go b/chainntnfs/bitcoindnotify/driver.go new file mode 100644 index 00000000..52dae673 --- /dev/null +++ b/chainntnfs/bitcoindnotify/driver.go @@ -0,0 +1,53 @@ +package bitcoindnotify + +import ( + "fmt" + + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcd/rpcclient" +) + +// createNewNotifier creates a new instance of the ChainNotifier interface +// implemented by BitcoindNotifier. +func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) { + if len(args) != 3 { + return nil, fmt.Errorf("incorrect number of arguments to "+ + ".New(...), expected 3, instead passed %v", len(args)) + } + + config, ok := args[0].(*rpcclient.ConnConfig) + if !ok { + return nil, fmt.Errorf("first argument to bitcoindnotifier." + + "New is incorrect, expected a *rpcclient.ConnConfig") + } + + zmqConnect, ok := args[1].(string) + if !ok { + return nil, fmt.Errorf("second argument to bitcoindnotifier." + + "New is incorrect, expected a string") + } + + params, ok := args[2].(chaincfg.Params) + if !ok { + return nil, fmt.Errorf("third argument to bitcoindnotifier." + + "New is incorrect, expected a chaincfg.Params") + } + + return New(config, zmqConnect, params) +} + +// init registers a driver for the BtcdNotifier concrete implementation of the +// chainntnfs.ChainNotifier interface. +func init() { + // Register the driver. + notifier := &chainntnfs.NotifierDriver{ + NotifierType: notifierType, + New: createNewNotifier, + } + + if err := chainntnfs.RegisterNotifier(notifier); err != nil { + panic(fmt.Sprintf("failed to register notifier driver '%s': %v", + notifierType, err)) + } +} diff --git a/chainntnfs/interface_test.go b/chainntnfs/interface_test.go index dc859926..1a1635dc 100644 --- a/chainntnfs/interface_test.go +++ b/chainntnfs/interface_test.go @@ -5,7 +5,9 @@ import ( "fmt" "io/ioutil" "log" + "math/rand" "os" + "os/exec" "path/filepath" "sync" "testing" @@ -13,6 +15,7 @@ import ( "github.com/lightninglabs/neutrino" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/ltcsuite/ltcd/btcjson" "github.com/roasbeef/btcd/chaincfg/chainhash" "github.com/roasbeef/btcwallet/walletdb" @@ -24,6 +27,10 @@ import ( "github.com/roasbeef/btcd/wire" "github.com/roasbeef/btcutil" + // Required to auto-register the bitcoind backed ChainNotifier + // implementation. + _ "github.com/lightningnetwork/lnd/chainntnfs/bitcoindnotify" + // Required to auto-register the btcd backed ChainNotifier // implementation. _ "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify" @@ -32,7 +39,8 @@ import ( // implementation. _ "github.com/lightningnetwork/lnd/chainntnfs/neutrinonotify" - _ "github.com/roasbeef/btcwallet/walletdb/bdb" // Required to register the boltdb walletdb implementation. + // Required to register the boltdb walletdb implementation. + _ "github.com/roasbeef/btcwallet/walletdb/bdb" ) var ( @@ -43,7 +51,7 @@ var ( 0x1e, 0xb, 0x4c, 0xfd, 0x9e, 0xc5, 0x8c, 0xe9, } - netParams = &chaincfg.SimNetParams + netParams = &chaincfg.RegressionNetParams privKey, pubKey = btcec.PrivKeyFromBytes(btcec.S256(), testPrivKey) addrPk, _ = btcutil.NewAddressPubKey(pubKey.SerializeCompressed(), netParams) @@ -65,6 +73,39 @@ func getTestTxId(miner *rpctest.Harness) (*chainhash.Hash, error) { return miner.SendOutputs(outputs, 10) } +func waitForMempoolTx(r *rpctest.Harness, txid *chainhash.Hash) error { + var found bool + var tx *btcutil.Tx + var err error + timeout := time.After(10 * time.Second) + for !found { + // Do a short wait + select { + case <-timeout: + return fmt.Errorf("timeout after 10s") + default: + } + time.Sleep(100 * time.Millisecond) + + // Check for the harness' knowledge of the txid + tx, err = r.Node.GetRawTransaction(txid) + if err != nil { + switch e := err.(type) { + case *btcjson.RPCError: + if e.Code == btcjson.ErrRPCNoTxInfo { + continue + } + default: + } + return err + } + if tx != nil && tx.MsgTx().TxHash() == *txid { + found = true + } + } + return nil +} + func testSingleConfirmationNotification(miner *rpctest.Harness, notifier chainntnfs.ChainNotifier, t *testing.T) { @@ -80,6 +121,11 @@ func testSingleConfirmationNotification(miner *rpctest.Harness, t.Fatalf("unable to create test tx: %v", err) } + err = waitForMempoolTx(miner, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + _, currentHeight, err := miner.Node.GetBestBlock() if err != nil { t.Fatalf("unable to get current height: %v", err) @@ -143,6 +189,11 @@ func testMultiConfirmationNotification(miner *rpctest.Harness, t.Fatalf("unable to create test addr: %v", err) } + err = waitForMempoolTx(miner, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + _, currentHeight, err := miner.Node.GetBestBlock() if err != nil { t.Fatalf("unable to get current height: %v", err) @@ -201,6 +252,11 @@ func testBatchConfirmationNotification(miner *rpctest.Harness, t.Fatalf("unable to register ntfn: %v", err) } confIntents[i] = confIntent + err = waitForMempoolTx(miner, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + } initialConfHeight := uint32(currentHeight + 1) @@ -252,6 +308,11 @@ func createSpendableOutput(miner *rpctest.Harness, t.Fatalf("unable to create test addr: %v", err) } + err = waitForMempoolTx(miner, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + // Mine a single block which should include that txid above. if _, err := miner.Node.Generate(1); err != nil { t.Fatalf("unable to generate single block: %v", err) @@ -342,6 +403,11 @@ func testSpendNotification(miner *rpctest.Harness, t.Fatalf("unable to broadcast tx: %v", err) } + err = waitForMempoolTx(miner, spenderSha) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + // Now we mine a single block, which should include our spend. The // notification should also be sent off. if _, err := miner.Node.Generate(1); err != nil { @@ -453,6 +519,11 @@ func testMultiClientConfirmationNotification(miner *rpctest.Harness, t.Fatalf("unable to create test tx: %v", err) } + err = waitForMempoolTx(miner, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + var wg sync.WaitGroup const ( numConfsClients = 5 @@ -514,6 +585,11 @@ func testTxConfirmedBeforeNtfnRegistration(miner *rpctest.Harness, t.Fatalf("unable to create test tx: %v", err) } + err = waitForMempoolTx(miner, txid3) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + // Generate another block containing tx 3, but we won't register conf // notifications for this tx until much later. The notifier must check // older blocks when the confirmation event is registered below to ensure @@ -529,11 +605,21 @@ func testTxConfirmedBeforeNtfnRegistration(miner *rpctest.Harness, t.Fatalf("unable to create test tx: %v", err) } + err = waitForMempoolTx(miner, txid1) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + txid2, err := getTestTxId(miner) if err != nil { t.Fatalf("unable to create test tx: %v", err) } + err = waitForMempoolTx(miner, txid2) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + _, currentHeight, err := miner.Node.GetBestBlock() if err != nil { t.Fatalf("unable to get current height: %v", err) @@ -654,6 +740,11 @@ func testLazyNtfnConsumer(miner *rpctest.Harness, t.Fatalf("unable to create test tx: %v", err) } + err = waitForMempoolTx(miner, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + _, currentHeight, err := miner.Node.GetBestBlock() if err != nil { t.Fatalf("unable to get current height: %v", err) @@ -686,6 +777,11 @@ func testLazyNtfnConsumer(miner *rpctest.Harness, t.Fatalf("unable to create test tx: %v", err) } + err = waitForMempoolTx(miner, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + _, currentHeight, err = miner.Node.GetBestBlock() if err != nil { t.Fatalf("unable to get current height: %v", err) @@ -736,6 +832,11 @@ func testSpendBeforeNtfnRegistration(miner *rpctest.Harness, t.Fatalf("unable to create test addr: %v", err) } + err = waitForMempoolTx(miner, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + // Mine a single block which should include that txid above. if _, err := miner.Node.Generate(1); err != nil { t.Fatalf("unable to generate single block: %v", err) @@ -789,6 +890,11 @@ func testSpendBeforeNtfnRegistration(miner *rpctest.Harness, t.Fatalf("unable to brodacst tx: %v", err) } + err = waitForMempoolTx(miner, spenderSha) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + // Now we mine an additional block, which should include our spend. if _, err := miner.Node.Generate(1); err != nil { t.Fatalf("unable to generate single block: %v", err) @@ -877,6 +983,11 @@ func testCancelSpendNtfn(node *rpctest.Harness, t.Fatalf("unable to brodacst tx: %v", err) } + err = waitForMempoolTx(node, spenderSha) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + // Now we mine a single block, which should include our spend. The // notification should also be sent off. if _, err := node.Node.Generate(1); err != nil { @@ -1021,6 +1132,11 @@ func testReorgConf(miner *rpctest.Harness, notifier chainntnfs.ChainNotifier, t.Fatalf("unable to create test tx: %v", err) } + err = waitForMempoolTx(miner, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + _, currentHeight, err := miner.Node.GetBestBlock() if err != nil { t.Fatalf("unable to get current height: %v", err) @@ -1094,11 +1210,16 @@ func testReorgConf(miner *rpctest.Harness, notifier chainntnfs.ChainNotifier, t.Fatalf("unable to get raw tx: %v", err) } - _, err = miner2.Node.SendRawTransaction(tx.MsgTx(), false) + txid, err = miner2.Node.SendRawTransaction(tx.MsgTx(), false) if err != nil { t.Fatalf("unable to get send tx: %v", err) } + err = waitForMempoolTx(miner, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } + _, err = miner.Node.Generate(3) if err != nil { t.Fatalf("unable to generate single block: %v", err) @@ -1206,12 +1327,73 @@ func TestInterfaces(t *testing.T) { switch notifierType { + case "bitcoind": + // Start a bitcoind instance. + tempBitcoindDir, err := ioutil.TempDir("", "bitcoind") + if err != nil { + t.Fatalf("Unable to create temp dir: %v", err) + } + zmqPath := "ipc:///" + tempBitcoindDir + "/weks.socket" + cleanUp1 := func() { + os.RemoveAll(tempBitcoindDir) + } + cleanUp = cleanUp1 + rpcPort := rand.Int()%(65536-1024) + 1024 + bitcoind := exec.Command( + "bitcoind", + "-datadir="+tempBitcoindDir, + "-regtest", + "-connect="+p2pAddr, + "-txindex", + "-rpcauth=weks:469e9bb14ab2360f8e226efed5ca6f"+ + "d$507c670e800a95284294edb5773b05544b"+ + "220110063096c221be9933c82d38e1", + fmt.Sprintf("-rpcport=%d", rpcPort), + "-disablewallet", + "-zmqpubrawblock="+zmqPath, + "-zmqpubrawtx="+zmqPath, + ) + err = bitcoind.Start() + if err != nil { + cleanUp1() + t.Fatalf("Couldn't start bitcoind: %v", err) + } + cleanUp2 := func() { + bitcoind.Process.Kill() + bitcoind.Wait() + cleanUp1() + } + cleanUp = cleanUp2 + + // Wait for the bitcoind instance to start up. + time.Sleep(time.Second) + + // Start the FilteredChainView implementation instance. + config := rpcclient.ConnConfig{ + Host: fmt.Sprintf( + "127.0.0.1:%d", rpcPort), + User: "weks", + Pass: "weks", + DisableAutoReconnect: false, + DisableConnectOnNew: true, + DisableTLS: true, + HTTPPostMode: true, + } + + notifier, err = notifierDriver.New(&config, zmqPath, + *netParams) + if err != nil { + t.Fatalf("unable to create %v notifier: %v", + notifierType, err) + } + case "btcd": notifier, err = notifierDriver.New(&rpcConfig) if err != nil { t.Fatalf("unable to create %v notifier: %v", notifierType, err) } + cleanUp = func() {} case "neutrino": spvDir, err := ioutil.TempDir("", "neutrino") diff --git a/glide.lock b/glide.lock index 9113b471..b6721ecc 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 8438e391bed32638a8e14402992af50b7656b380f69741cf8422584c2e1f2b31 -updated: 2017-12-13T15:13:22.311098343-08:00 +hash: d145c16f2f9cfdf4937eb8b7cdd65919a8c351593a179acc23f2cbca5b42f34b +updated: 2017-12-22T23:45:25.148488338-07:00 imports: - name: github.com/aead/chacha20 version: d31a916ded42d1640b9d89a26f8abd53cc96790c @@ -86,6 +86,8 @@ imports: version: 946bd9fbed05568b0f3cd188353d8aa28f38b688 subpackages: - internal/socket +- name: github.com/pebbe/zmq4 + version: 90d69e412a09549f2e90bac70fbb449081f1e5c1 - name: github.com/roasbeef/btcd version: 9978b939c33973be19b932fa7b936079bb7ba38d subpackages: diff --git a/glide.yaml b/glide.yaml index 00ef7b0c..0174fc59 100644 --- a/glide.yaml +++ b/glide.yaml @@ -24,6 +24,7 @@ import: - txscript - wire - connmgr +- package: github.com/pebbe/zmq4 - package: github.com/roasbeef/btcrpcclient version: d0f4db8b4dad0ca3d569b804f21247c3dd96acbb - package: github.com/roasbeef/btcutil diff --git a/lnwallet/btcwallet/blockchain.go b/lnwallet/btcwallet/blockchain.go index d930ac55..39f73b01 100644 --- a/lnwallet/btcwallet/blockchain.go +++ b/lnwallet/btcwallet/blockchain.go @@ -29,23 +29,7 @@ var ( // // This method is a part of the lnwallet.BlockChainIO interface. func (b *BtcWallet) GetBestBlock() (*chainhash.Hash, int32, error) { - switch backend := b.chain.(type) { - - case *chain.NeutrinoClient: - header, height, err := backend.CS.BlockHeaders.ChainTip() - if err != nil { - return nil, -1, err - } - - blockHash := header.BlockHash() - return &blockHash, int32(height), nil - - case *chain.RPCClient: - return backend.GetBestBlock() - - default: - return nil, -1, fmt.Errorf("unknown backend") - } + return b.chain.GetBestBlock() } // GetUtxo returns the original output referenced by the passed outpoint. @@ -100,6 +84,26 @@ func (b *BtcWallet) GetUtxo(op *wire.OutPoint, heightHint uint32) (*wire.TxOut, PkScript: pkScript, }, nil + case *chain.BitcoindClient: + txout, err := backend.GetTxOut(&op.Hash, op.Index, false) + if err != nil { + return nil, err + } else if txout == nil { + return nil, ErrOutputSpent + } + + pkScript, err := hex.DecodeString(txout.ScriptPubKey.Hex) + if err != nil { + return nil, err + } + + return &wire.TxOut{ + // Sadly, gettxout returns the output value in BTC + // instead of satoshis. + Value: int64(txout.Value * 1e8), + PkScript: pkScript, + }, nil + default: return nil, fmt.Errorf("unknown backend") } @@ -109,27 +113,7 @@ func (b *BtcWallet) GetUtxo(op *wire.OutPoint, heightHint uint32) (*wire.TxOut, // // This method is a part of the lnwallet.BlockChainIO interface. func (b *BtcWallet) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) { - switch backend := b.chain.(type) { - - case *chain.NeutrinoClient: - block, err := backend.CS.GetBlockFromNetwork(*blockHash) - if err != nil { - return nil, err - } - - return block.MsgBlock(), nil - - case *chain.RPCClient: - block, err := backend.GetBlock(blockHash) - if err != nil { - return nil, err - } - - return block, nil - - default: - return nil, fmt.Errorf("unknown backend") - } + return b.chain.GetBlock(blockHash) } // GetBlockHash returns the hash of the block in the best blockchain at the @@ -137,29 +121,7 @@ func (b *BtcWallet) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) // // This method is a part of the lnwallet.BlockChainIO interface. func (b *BtcWallet) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) { - switch backend := b.chain.(type) { - - case *chain.NeutrinoClient: - height := uint32(blockHeight) - blockHeader, err := backend.CS.BlockHeaders.FetchHeaderByHeight(height) - if err != nil { - return nil, err - } - - blockHash := blockHeader.BlockHash() - return &blockHash, nil - - case *chain.RPCClient: - blockHash, err := backend.GetBlockHash(blockHeight) - if err != nil { - return nil, err - } - - return blockHash, nil - - default: - return nil, fmt.Errorf("unknown backend") - } + return b.chain.GetBlockHash(blockHeight) } // A compile time check to ensure that BtcWallet implements the BlockChainIO diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index 72c783b1..72bba4c9 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -119,6 +119,17 @@ func New(cfg Config) (*BtcWallet, error) { }, nil } +// BackEnd returns the underlying ChainService's name as a string. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) BackEnd() string { + if b.chain != nil { + return b.chain.BackEnd() + } + + return "" +} + // Start initializes the underlying rpc connection, the wallet itself, and // begins syncing to the current available blockchain state. // @@ -668,22 +679,9 @@ func (b *BtcWallet) IsSynced() (bool, error) { // Next, query the chain backend to grab the info about the tip of the // main chain. - switch backend := b.cfg.ChainSource.(type) { - case *chain.NeutrinoClient: - header, height, err := backend.CS.BlockHeaders.ChainTip() - if err != nil { - return false, err - } - - bh := header.BlockHash() - bestHash = &bh - bestHeight = int32(height) - - case *chain.RPCClient: - bestHash, bestHeight, err = backend.GetBestBlock() - if err != nil { - return false, err - } + bestHash, bestHeight, err = b.cfg.ChainSource.GetBestBlock() + if err != nil { + return false, err } // If the wallet hasn't yet fully synced to the node's best chain tip, @@ -696,21 +694,9 @@ func (b *BtcWallet) IsSynced() (bool, error) { // still may not yet be synced as the chain backend may still be // catching up to the main chain. So we'll grab the block header in // order to make a guess based on the current time stamp. - var blockHeader *wire.BlockHeader - switch backend := b.cfg.ChainSource.(type) { - - case *chain.NeutrinoClient: - bh, _, err := backend.CS.BlockHeaders.FetchHeader(bestHash) - if err != nil { - return false, err - } - blockHeader = bh - - case *chain.RPCClient: - blockHeader, err = backend.GetBlockHeader(bestHash) - if err != nil { - return false, err - } + blockHeader, err := b.cfg.ChainSource.GetBlockHeader(bestHash) + if err != nil { + return false, err } // If the timestamp no the best header is more than 2 hours in the diff --git a/lnwallet/btcwallet/driver.go b/lnwallet/btcwallet/driver.go index 16078572..91f64973 100644 --- a/lnwallet/btcwallet/driver.go +++ b/lnwallet/btcwallet/driver.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/roasbeef/btcwallet/chain" ) const ( @@ -36,6 +37,7 @@ func init() { driver := &lnwallet.WalletDriver{ WalletType: walletType, New: createNewWallet, + BackEnds: chain.BackEnds, } if err := lnwallet.RegisterWallet(driver); err != nil { diff --git a/lnwallet/fee_estimator.go b/lnwallet/fee_estimator.go index a36fa81a..c98adf29 100644 --- a/lnwallet/fee_estimator.go +++ b/lnwallet/fee_estimator.go @@ -1,6 +1,8 @@ package lnwallet import ( + "encoding/json" + "github.com/roasbeef/btcd/blockchain" "github.com/roasbeef/btcd/rpcclient" "github.com/roasbeef/btcutil" @@ -200,3 +202,145 @@ func (b *BtcdFeeEstimator) fetchEstimatePerByte(confTarget uint32) (btcutil.Amou // A compile-time assertion to ensure that BtcdFeeEstimator implements the // FeeEstimator interface. var _ FeeEstimator = (*BtcdFeeEstimator)(nil) + +// BitcoindFeeEstimator is an implementation of the FeeEstimator interface +// backed by the RPC interface of an active bitcoind node. This implementation +// will proxy any fee estimation requests to bitcoind's RPC interace. +type BitcoindFeeEstimator struct { + // fallBackFeeRate is the fall back fee rate in satoshis per byte that + // is returned if the fee estimator does not yet have enough data to + // actually produce fee estimates. + fallBackFeeRate btcutil.Amount + + bitcoindConn *rpcclient.Client +} + +// NewBitcoindFeeEstimator creates a new BitcoindFeeEstimator given a fully +// populated rpc config that is able to successfully connect and authenticate +// with the bitcoind node, and also a fall back fee rate. The fallback fee rate +// is used in the occasion that the estimator has insufficient data, or returns +// zero for a fee estimate. +func NewBitcoindFeeEstimator(rpcConfig rpcclient.ConnConfig, + fallBackFeeRate btcutil.Amount) (*BitcoindFeeEstimator, error) { + + rpcConfig.DisableConnectOnNew = true + rpcConfig.DisableAutoReconnect = false + rpcConfig.DisableTLS = true + rpcConfig.HTTPPostMode = true + chainConn, err := rpcclient.New(&rpcConfig, nil) + if err != nil { + return nil, err + } + + return &BitcoindFeeEstimator{ + fallBackFeeRate: fallBackFeeRate, + bitcoindConn: chainConn, + }, nil +} + +// Start signals the FeeEstimator to start any processes or goroutines +// it needs to perform its duty. +// +// NOTE: This method is part of the FeeEstimator interface. +func (b *BitcoindFeeEstimator) Start() error { + return nil +} + +// Stop stops any spawned goroutines and cleans up the resources used +// by the fee estimator. +// +// NOTE: This method is part of the FeeEstimator interface. +func (b *BitcoindFeeEstimator) Stop() error { + return nil +} + +// EstimateFeePerByte takes in a target for the number of blocks until an +// initial confirmation and returns the estimated fee expressed in +// satoshis/byte. +func (b *BitcoindFeeEstimator) EstimateFeePerByte(numBlocks uint32) (btcutil.Amount, error) { + feeEstimate, err := b.fetchEstimatePerByte(numBlocks) + switch { + // If the estimator doesn't have enough data, or returns an error, then + // to return a proper value, then we'll return the default fall back + // fee rate. + case err != nil: + walletLog.Errorf("unable to query estimator: %v", err) + fallthrough + + case feeEstimate == 0: + return b.fallBackFeeRate, nil + } + + return feeEstimate, nil +} + +// EstimateFeePerWeight takes in a target for the number of blocks until an +// initial confirmation and returns the estimated fee expressed in +// satoshis/weight. +func (b *BitcoindFeeEstimator) EstimateFeePerWeight(numBlocks uint32) (btcutil.Amount, error) { + feePerByte, err := b.EstimateFeePerByte(numBlocks) + if err != nil { + return 0, err + } + + // We'll scale down the fee per byte to fee per weight, as for each raw + // byte, there's 1/4 unit of weight mapped to it. + satWeight := feePerByte / blockchain.WitnessScaleFactor + + // If this ends up scaling down to a zero sat/weight amount, then we'll + // use the default fallback fee rate. + // TODO(aakselrod): maybe use the per-byte rate if it's non-zero? + // Otherwise, we can return a higher sat/byte than sat/weight. + if satWeight == 0 { + return b.fallBackFeeRate / blockchain.WitnessScaleFactor, nil + } + + return satWeight, nil +} + +// fetchEstimate returns a fee estimate for a transaction be be confirmed in +// confTarget blocks. The estimate is returned in sat/byte. +func (b *BitcoindFeeEstimator) fetchEstimatePerByte(confTarget uint32) (btcutil.Amount, error) { + // First, we'll send an "estimatesmartfee" command as a raw request, + // since it isn't supported by btcd but is available in bitcoind. + target, err := json.Marshal(uint64(confTarget)) + if err != nil { + return 0, err + } + // TODO: Allow selection of economical/conservative modifiers. + resp, err := b.bitcoindConn.RawRequest("estimatesmartfee", + []json.RawMessage{target}) + if err != nil { + return 0, err + } + + // Next, we'll parse the response to get the BTC per KB. + feeEstimate := struct { + Feerate float64 `json:"feerate"` + }{} + err = json.Unmarshal(resp, &feeEstimate) + if err != nil { + return 0, err + } + + // Next, we'll convert the returned value to satoshis, as it's + // currently returned in BTC. + satPerKB, err := btcutil.NewAmount(feeEstimate.Feerate) + if err != nil { + return 0, err + } + + // The value returned is expressed in fees per KB, while we want + // fee-per-byte, so we'll divide by 1024 to map to satoshis-per-byte + // before returning the estimate. + satPerByte := satPerKB / 1024 + + walletLog.Debugf("Returning %v sat/byte for conf target of %v", + int64(satPerByte), confTarget) + + return satPerByte, nil +} + +// A compile-time assertion to ensure that BitcoindFeeEstimator implements the +// FeeEstimator interface. +var _ FeeEstimator = (*BitcoindFeeEstimator)(nil) diff --git a/lnwallet/interface.go b/lnwallet/interface.go index c257d3e6..88b261e0 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -207,6 +207,11 @@ type WalletController interface { // Stop signals the wallet for shutdown. Shutdown may entail closing // any active sockets, database handles, stopping goroutines, etc. Stop() error + + // BackEnd returns a name for the wallet's backing chain service, + // which could be e.g. btcd, bitcoind, neutrino, or another consensus + // service. + BackEnd() string } // BlockChainIO is a dedicated source which will be used to obtain queries @@ -288,6 +293,10 @@ type WalletDriver struct { // initialization flexibility, thereby accommodating several potential // WalletController implementations. New func(args ...interface{}) (WalletController, error) + + // BackEnds returns a list of available chain service drivers for the + // wallet driver. This could be e.g. bitcoind, btcd, neutrino, etc. + BackEnds func() []string } var ( diff --git a/lnwallet/interface_test.go b/lnwallet/interface_test.go index 9d09d595..7ba38e34 100644 --- a/lnwallet/interface_test.go +++ b/lnwallet/interface_test.go @@ -5,8 +5,10 @@ import ( "encoding/hex" "fmt" "io/ioutil" + "math/rand" "net" "os" + "os/exec" "path/filepath" "reflect" "runtime" @@ -17,7 +19,10 @@ import ( "github.com/boltdb/bolt" "github.com/davecgh/go-spew/spew" + "github.com/lightninglabs/neutrino" "github.com/roasbeef/btcwallet/chain" + "github.com/roasbeef/btcwallet/walletdb" + _ "github.com/roasbeef/btcwallet/walletdb/bdb" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify" @@ -25,10 +30,10 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/lightningnetwork/lnd/lnwire" + "github.com/roasbeef/btcd/btcjson" "github.com/roasbeef/btcd/chaincfg" "github.com/roasbeef/btcd/chaincfg/chainhash" "github.com/roasbeef/btcd/rpcclient" - _ "github.com/roasbeef/btcwallet/walletdb/bdb" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/integration/rpctest" @@ -76,7 +81,7 @@ var ( 0x69, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53, } - netParams = &chaincfg.SimNetParams + netParams = &chaincfg.RegressionNetParams chainHash = netParams.GenesisHash _, alicePub = btcec.PrivKeyFromBytes(btcec.S256(), testHdSeed[:]) @@ -145,6 +150,12 @@ func calcStaticFee(numHTLCs int) btcutil.Amount { func loadTestCredits(miner *rpctest.Harness, w *lnwallet.LightningWallet, numOutputs, btcPerOutput int) error { + // For initial neutrino connection, wait a second. + // TODO(aakselrod): Eliminate the need for this. + switch w.BackEnd() { + case "neutrino": + time.Sleep(time.Second) + } // Using the mining node, spend from a coinbase output numOutputs to // give us btcPerOutput with each output. satoshiPerOutput := int64(btcPerOutput * 1e8) @@ -188,6 +199,7 @@ func loadTestCredits(miner *rpctest.Harness, w *lnwallet.LightningWallet, // Wait until the wallet has finished syncing up to the main chain. ticker := time.NewTicker(100 * time.Millisecond) + timeout := time.After(30 * time.Second) for range ticker.C { balance, err := w.ConfirmedBalance(1, false) @@ -197,6 +209,17 @@ func loadTestCredits(miner *rpctest.Harness, w *lnwallet.LightningWallet, if balance == expectedBalance { break } + select { + case <-timeout: + synced, err := w.IsSynced() + if err != nil { + return err + } + return fmt.Errorf("timed out after 30 seconds "+ + "waiting for balance %v, current balance %v, "+ + "synced: %t", expectedBalance, balance, synced) + default: + } } ticker.Stop() @@ -222,7 +245,7 @@ func createTestWallet(tempTestDir string, miningNode *rpctest.Harness, WalletController: wc, Signer: signer, ChainIO: bio, - FeeEstimator: lnwallet.StaticFeeEstimator{FeeRate: 250}, + FeeEstimator: lnwallet.StaticFeeEstimator{FeeRate: 10}, DefaultConstraints: channeldb.ChannelConstraints{ DustLimit: 500, MaxPendingAmount: lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin) * 100, @@ -343,6 +366,9 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, bobFundingSigs, bobCommitSig, ) if err != nil { + for _, in := range aliceChanReservation.FinalFundingTx().TxIn { + fmt.Println(in.PreviousOutPoint.String()) + } t.Fatalf("unable to consume alice's sigs: %v", err) } _, err = bobChanReservation.CompleteReservation( @@ -384,6 +410,10 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, // Mine a single block, the funding transaction should be included // within this block. + err = waitForMempoolTx(miner, &fundingSha) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } blockHashes, err := miner.Node.Generate(1) if err != nil { t.Fatalf("unable to generate block: %v", err) @@ -402,6 +432,16 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, assertReservationDeleted(aliceChanReservation, t) assertReservationDeleted(bobChanReservation, t) + + // Wait for wallets to catch up to prevent issues in subsequent tests. + err = waitForWalletSync(miner, alice) + if err != nil { + t.Fatalf("unable to sync alice: %v", err) + } + err = waitForWalletSync(miner, bob) + if err != nil { + t.Fatalf("unable to sync bob: %v", err) + } } func testFundingTransactionLockedOutputs(miner *rpctest.Harness, @@ -759,6 +799,10 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, // Mine a single block, the funding transaction should be included // within this block. + err = waitForMempoolTx(miner, &fundingSha) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } blockHashes, err := miner.Node.Generate(1) if err != nil { t.Fatalf("unable to generate block: %v", err) @@ -768,7 +812,8 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, t.Fatalf("unable to find block: %v", err) } if len(block.Transactions) != 2 { - t.Fatalf("funding transaction wasn't mined: %v", err) + t.Fatalf("funding transaction wasn't mined: %d", + len(block.Transactions)) } blockTx := block.Transactions[1] if blockTx.TxHash() != fundingSha { @@ -815,8 +860,10 @@ func testListTransactionDetails(miner *rpctest.Harness, } // Next, fetch all the current transaction details. - // TODO(roasbeef): use ntfn client here instead? - time.Sleep(time.Second * 2) + err = waitForWalletSync(miner, alice) + if err != nil { + t.Fatalf("Couldn't sync Alice's wallet: %v", err) + } txDetails, err := alice.ListTransactionDetails() if err != nil { t.Fatalf("unable to fetch tx details: %v", err) @@ -905,6 +952,10 @@ func testListTransactionDetails(miner *rpctest.Harness, if err != nil { t.Fatalf("unable to create burn tx: %v", err) } + err = waitForMempoolTx(miner, burnTXID) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } burnBlock, err := miner.Node.Generate(1) if err != nil { t.Fatalf("unable to mine block: %v", err) @@ -912,7 +963,10 @@ func testListTransactionDetails(miner *rpctest.Harness, // Fetch the transaction details again, the new transaction should be // shown as debiting from the wallet's balance. - time.Sleep(time.Second * 2) + err = waitForWalletSync(miner, alice) + if err != nil { + t.Fatalf("Couldn't sync Alice's wallet: %v", err) + } txDetails, err = alice.ListTransactionDetails() if err != nil { t.Fatalf("unable to fetch tx details: %v", err) @@ -955,7 +1009,7 @@ func testTransactionSubscriptions(miner *rpctest.Harness, // implementation of the WalletController. txClient, err := alice.SubscribeTransactions() if err != nil { - t.Fatalf("unable to generate tx subscription: %v", err) + t.Skipf("unable to generate tx subscription: %v", err) } defer txClient.Cancel() @@ -964,25 +1018,33 @@ func testTransactionSubscriptions(miner *rpctest.Harness, numTxns = 3 ) unconfirmedNtfns := make(chan struct{}) - go func() { - for i := 0; i < numTxns; i++ { - txDetail := <-txClient.UnconfirmedTransactions() - if txDetail.NumConfirmations != 0 { - t.Fatalf("incorrect number of confs, expected %v got %v", - 0, txDetail.NumConfirmations) + switch alice.BackEnd() { + case "neutrino": + // Neutrino doesn't listen for unconfirmed transactions. + default: + go func() { + for i := 0; i < numTxns; i++ { + txDetail := <-txClient.UnconfirmedTransactions() + if txDetail.NumConfirmations != 0 { + t.Fatalf("incorrect number of confs, "+ + "expected %v got %v", 0, + txDetail.NumConfirmations) + } + if txDetail.Value != outputAmt { + t.Fatalf("incorrect output amt, "+ + "expected %v got %v", outputAmt, + txDetail.Value) + } + if txDetail.BlockHash != nil { + t.Fatalf("block hash should be nil, "+ + "is instead %v", + txDetail.BlockHash) + } } - if txDetail.Value != outputAmt { - t.Fatalf("incorrect output amt, expected %v got %v", - outputAmt, txDetail.Value) - } - if txDetail.BlockHash != nil { - t.Fatalf("block hash should be nil, is instead %v", - txDetail.BlockHash) - } - } - close(unconfirmedNtfns) - }() + close(unconfirmedNtfns) + }() + } // Next, fetch a fresh address from the wallet, create 3 new outputs // with the pkScript. @@ -1000,17 +1062,27 @@ func testTransactionSubscriptions(miner *rpctest.Harness, Value: outputAmt, PkScript: script, } - if _, err := miner.SendOutputs([]*wire.TxOut{output}, 10); err != nil { + txid, err := miner.SendOutputs([]*wire.TxOut{output}, 10) + if err != nil { t.Fatalf("unable to send coinbase: %v", err) } + err = waitForMempoolTx(miner, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } } - // We should receive a notification for all three transactions - // generated above. - select { - case <-time.After(time.Second * 5): - t.Fatalf("transactions not received after 3 seconds") - case <-unconfirmedNtfns: // Fall through on successs + switch alice.BackEnd() { + case "neutrino": + // Neutrino doesn't listen for on unconfirmed transactions. + default: + // We should receive a notification for all three transactions + // generated above. + select { + case <-time.After(time.Second * 10): + t.Fatalf("transactions not received after 10 seconds") + case <-unconfirmedNtfns: // Fall through on successs + } } confirmedNtfns := make(chan struct{}) @@ -1018,12 +1090,12 @@ func testTransactionSubscriptions(miner *rpctest.Harness, for i := 0; i < numTxns; i++ { txDetail := <-txClient.ConfirmedTransactions() if txDetail.NumConfirmations != 1 { - t.Fatalf("incorrect number of confs, expected %v got %v", - 1, txDetail.NumConfirmations) + t.Fatalf("incorrect number of confs for %s, expected %v got %v", + txDetail.Hash, 1, txDetail.NumConfirmations) } if txDetail.Value != outputAmt { - t.Fatalf("incorrect output amt, expected %v got %v", - outputAmt, txDetail.Value) + t.Fatalf("incorrect output amt, expected %v got %v in txid %s", + outputAmt, txDetail.Value, txDetail.Hash) } } close(confirmedNtfns) @@ -1039,7 +1111,7 @@ func testTransactionSubscriptions(miner *rpctest.Harness, // since they should be mined in the next block. select { case <-time.After(time.Second * 5): - t.Fatalf("transactions not received after 3 seconds") + t.Fatalf("transactions not received after 5 seconds") case <-confirmedNtfns: // Fall through on success } } @@ -1088,7 +1160,7 @@ func testSignOutputUsingTweaks(r *rpctest.Harness, // generate a regular p2wkh from that. pubkeyHash := btcutil.Hash160(tweakedKey.SerializeCompressed()) keyAddr, err := btcutil.NewAddressWitnessPubKeyHash(pubkeyHash, - &chaincfg.SimNetParams) + &chaincfg.RegressionNetParams) if err != nil { t.Fatalf("unable to create addr: %v", err) } @@ -1110,6 +1182,10 @@ func testSignOutputUsingTweaks(r *rpctest.Harness, // Query for the transaction generated above so we can located // the index of our output. + err = waitForMempoolTx(r, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } tx, err := r.Node.GetRawTransaction(txid) if err != nil { t.Fatalf("unable to query for tx: %v", err) @@ -1197,7 +1273,7 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet, } // Give wallet time to catch up. - err = waitForWalletSync(w) + err = waitForWalletSync(r, w) if err != nil { t.Fatalf("unable to sync wallet: %v", err) } @@ -1222,16 +1298,21 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet, Value: 1e8, PkScript: script, } - if _, err = w.SendOutputs([]*wire.TxOut{output}, 10); err != nil { + txid, err := w.SendOutputs([]*wire.TxOut{output}, 10) + if err != nil { t.Fatalf("unable to send outputs: %v", err) } + err = waitForMempoolTx(r, txid) + if err != nil { + t.Fatalf("tx not relayed to miner: %v", err) + } _, err = r.Node.Generate(50) if err != nil { t.Fatalf("unable to generate blocks on passed node: %v", err) } // Give wallet time to catch up. - err = waitForWalletSync(w) + err = waitForWalletSync(r, w) if err != nil { t.Fatalf("unable to sync wallet: %v", err) } @@ -1277,32 +1358,34 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet, // one block on the passed miner and two on the created miner, // connecting them, and waiting for them to sync. for i := 0; i < 5; i++ { - peers, err := r2.Node.GetPeerInfo() - if err != nil { - t.Fatalf("unable to get peer info: %v", err) - } - numPeers := len(peers) - err = r2.Node.AddNode(r.P2PAddress(), rpcclient.ANRemove) - if err != nil { - t.Fatalf("unable to disconnect mining nodes: %v", err) - } // Wait for disconnection timeout := time.After(30 * time.Second) - for true { + stillConnected := true + var peers []btcjson.GetPeerInfoResult + for stillConnected { // Allow for timeout + time.Sleep(100 * time.Millisecond) select { case <-timeout: t.Fatalf("timeout waiting for miner disconnect") default: } + err = r2.Node.AddNode(r.P2PAddress(), rpcclient.ANRemove) + if err != nil { + t.Fatalf("unable to disconnect mining nodes: %v", + err) + } peers, err = r2.Node.GetPeerInfo() if err != nil { t.Fatalf("unable to get peer info: %v", err) } - if len(peers) < numPeers { - break + stillConnected = false + for _, peer := range peers { + if peer.Addr == r.P2PAddress() { + stillConnected = true + break + } } - time.Sleep(100 * time.Millisecond) } _, err = r.Node.Generate(2) if err != nil { @@ -1318,8 +1401,16 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet, // Step 5: Reconnect the miners and wait for them to synchronize. err = r2.Node.AddNode(r.P2PAddress(), rpcclient.ANAdd) if err != nil { - t.Fatalf("unable to connect mining nodes together: %v", - err) + switch err := err.(type) { + case *btcjson.RPCError: + if err.Code != -8 { + t.Fatalf("unable to connect mining "+ + "nodes together: %v", err) + } + default: + t.Fatalf("unable to connect mining nodes "+ + "together: %v", err) + } } err = rpctest.JoinNodes([]*rpctest.Harness{r2, r}, rpctest.Blocks) @@ -1328,7 +1419,7 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet, } // Give wallet time to catch up. - err = waitForWalletSync(w) + err = waitForWalletSync(r, w) if err != nil { t.Fatalf("unable to sync wallet: %v", err) } @@ -1405,21 +1496,78 @@ func clearWalletStates(a, b *lnwallet.LightningWallet) error { return b.Cfg.Database.Wipe() } -func waitForWalletSync(w *lnwallet.LightningWallet) error { - var synced bool +func waitForMempoolTx(r *rpctest.Harness, txid *chainhash.Hash) error { + var found bool + var tx *btcutil.Tx var err error timeout := time.After(10 * time.Second) - for !synced { - synced, err = w.IsSynced() - if err != nil { - return err - } + for !found { + // Do a short wait select { case <-timeout: return fmt.Errorf("timeout after 10s") default: } time.Sleep(100 * time.Millisecond) + + // Check for the harness' knowledge of the txid + tx, err = r.Node.GetRawTransaction(txid) + if err != nil { + switch e := err.(type) { + case *btcjson.RPCError: + if e.Code == btcjson.ErrRPCNoTxInfo { + continue + } + default: + } + return err + } + if tx != nil && tx.MsgTx().TxHash() == *txid { + found = true + } + } + return nil +} + +func waitForWalletSync(r *rpctest.Harness, w *lnwallet.LightningWallet) error { + var synced bool + var err error + var bestHash, knownHash *chainhash.Hash + var bestHeight, knownHeight int32 + timeout := time.After(10 * time.Second) + for !synced { + // Do a short wait + select { + case <-timeout: + return fmt.Errorf("timeout after 10s") + default: + } + time.Sleep(100 * time.Millisecond) + + // Check whether the chain source of the wallet is caught up to + // the harness it's supposed to be catching up to. + bestHash, bestHeight, err = r.Node.GetBestBlock() + if err != nil { + return err + } + knownHash, knownHeight, err = w.Cfg.ChainIO.GetBestBlock() + if err != nil { + return err + } + if knownHeight != bestHeight { + continue + } + if *knownHash != *bestHash { + return fmt.Errorf("hash at height %d doesn't match: "+ + "expected %s, got %s", bestHeight, bestHash, + knownHash) + } + + // Check for synchronization. + synced, err = w.IsSynced() + if err != nil { + return err + } } return nil } @@ -1454,7 +1602,7 @@ func TestLightningWallet(t *testing.T) { } // Next mine enough blocks in order for segwit and the CSV package - // soft-fork to activate on SimNet. + // soft-fork to activate on RegNet. numBlocks := netParams.MinerConfirmationWindow * 2 if _, err := miningNode.Node.Generate(numBlocks); err != nil { t.Fatalf("unable to generate blocks: %v", err) @@ -1470,6 +1618,22 @@ func TestLightningWallet(t *testing.T) { t.Fatalf("unable to start notifier: %v", err) } + for _, walletDriver := range lnwallet.RegisteredWallets() { + for _, backEnd := range walletDriver.BackEnds() { + runTests(t, walletDriver, backEnd, miningNode, + rpcConfig, chainNotifier) + } + } +} + +// runTests runs all of the tests for a single interface implementation and +// chain back-end combination. This makes it easier to use `defer` as well as +// factoring out the test logic from the loop which cycles through the +// interface implementations. +func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, + backEnd string, miningNode *rpctest.Harness, + rpcConfig rpcclient.ConnConfig, + chainNotifier *btcdnotify.BtcdNotifier) { var ( bio lnwallet.BlockChainIO @@ -1478,107 +1642,230 @@ func TestLightningWallet(t *testing.T) { aliceWalletController lnwallet.WalletController bobWalletController lnwallet.WalletController + + feeEstimator lnwallet.FeeEstimator ) - for _, walletDriver := range lnwallet.RegisteredWallets() { - tempTestDirAlice, err := ioutil.TempDir("", "lnwallet") - if err != nil { - t.Fatalf("unable to create temp directory: %v", err) - } - defer os.RemoveAll(tempTestDirAlice) - tempTestDirBob, err := ioutil.TempDir("", "lnwallet") - if err != nil { - t.Fatalf("unable to create temp directory: %v", err) - } - defer os.RemoveAll(tempTestDirBob) + tempTestDirAlice, err := ioutil.TempDir("", "lnwallet") + if err != nil { + t.Fatalf("unable to create temp directory: %v", err) + } + defer os.RemoveAll(tempTestDirAlice) - walletType := walletDriver.WalletType - switch walletType { - case "btcwallet": - aliceChainRPC, err := chain.NewRPCClient(netParams, + tempTestDirBob, err := ioutil.TempDir("", "lnwallet") + if err != nil { + t.Fatalf("unable to create temp directory: %v", err) + } + defer os.RemoveAll(tempTestDirBob) + + walletType := walletDriver.WalletType + switch walletType { + case "btcwallet": + var aliceClient, bobClient chain.Interface + switch backEnd { + case "btcd": + feeEstimator, err = lnwallet.NewBtcdFeeEstimator( + rpcConfig, 250) + if err != nil { + t.Fatalf("unable to create btcd fee estimator: %v", + err) + } + aliceClient, err = chain.NewRPCClient(netParams, rpcConfig.Host, rpcConfig.User, rpcConfig.Pass, rpcConfig.Certificates, false, 20) if err != nil { t.Fatalf("unable to make chain rpc: %v", err) } - aliceWalletConfig := &btcwallet.Config{ - PrivatePass: []byte("alice-pass"), - HdSeed: aliceHDSeed[:], - DataDir: tempTestDirAlice, - NetParams: netParams, - ChainSource: aliceChainRPC, - FeeEstimator: lnwallet.StaticFeeEstimator{FeeRate: 250}, - } - aliceWalletController, err = walletDriver.New(aliceWalletConfig) - if err != nil { - t.Fatalf("unable to create btcwallet: %v", err) - } - aliceSigner = aliceWalletController.(*btcwallet.BtcWallet) - - bobChainRPC, err := chain.NewRPCClient(netParams, + bobClient, err = chain.NewRPCClient(netParams, rpcConfig.Host, rpcConfig.User, rpcConfig.Pass, rpcConfig.Certificates, false, 20) if err != nil { t.Fatalf("unable to make chain rpc: %v", err) } - bobWalletConfig := &btcwallet.Config{ - PrivatePass: []byte("bob-pass"), - HdSeed: bobHDSeed[:], - DataDir: tempTestDirBob, - NetParams: netParams, - ChainSource: bobChainRPC, - FeeEstimator: lnwallet.StaticFeeEstimator{FeeRate: 250}, - } - bobWalletController, err = walletDriver.New(bobWalletConfig) + case "neutrino": + feeEstimator = lnwallet.StaticFeeEstimator{FeeRate: 250} + // Set some package-level variable to speed up + // operation for tests. + neutrino.WaitForMoreCFHeaders = time.Millisecond * 100 + neutrino.BanDuration = time.Millisecond * 100 + neutrino.QueryTimeout = time.Millisecond * 500 + neutrino.QueryNumRetries = 2 + // Start Alice - open a database, start a neutrino + // instance, and initialize a btcwallet driver for it. + aliceDB, err := walletdb.Create("bdb", + tempTestDirAlice+"/neutrino.db") if err != nil { - t.Fatalf("unable to create btcwallet: %v", err) + t.Fatalf("unable to create DB: %v", err) + } + defer aliceDB.Close() + aliceChain, err := neutrino.NewChainService( + neutrino.Config{ + DataDir: tempTestDirAlice, + Database: aliceDB, + Namespace: []byte("alice"), + ChainParams: *netParams, + ConnectPeers: []string{ + miningNode.P2PAddress(), + }, + }, + ) + if err != nil { + t.Fatalf("unable to make neutrino: %v", err) + } + aliceChain.Start() + defer aliceChain.Stop() + aliceClient = chain.NewNeutrinoClient(aliceChain) + + // Start Bob - open a database, start a neutrino + // instance, and initialize a btcwallet driver for it. + bobDB, err := walletdb.Create("bdb", + tempTestDirBob+"/neutrino.db") + if err != nil { + t.Fatalf("unable to create DB: %v", err) + } + defer bobDB.Close() + bobChain, err := neutrino.NewChainService( + neutrino.Config{ + DataDir: tempTestDirBob, + Database: bobDB, + Namespace: []byte("bob"), + ChainParams: *netParams, + ConnectPeers: []string{ + miningNode.P2PAddress(), + }, + }, + ) + if err != nil { + t.Fatalf("unable to make neutrino: %v", err) + } + bobChain.Start() + defer bobChain.Stop() + bobClient = chain.NewNeutrinoClient(bobChain) + case "bitcoind": + feeEstimator, err = lnwallet.NewBitcoindFeeEstimator( + rpcConfig, 250) + if err != nil { + t.Fatalf("unable to create bitcoind fee estimator: %v", + err) + } + // Start a bitcoind instance. + tempBitcoindDir, err := ioutil.TempDir("", "bitcoind") + if err != nil { + t.Fatalf("unable to create temp directory: %v", err) + } + zmqPath := "ipc:///" + tempBitcoindDir + "/weks.socket" + defer os.RemoveAll(tempBitcoindDir) + rpcPort := rand.Int()%(65536-1024) + 1024 + bitcoind := exec.Command( + "bitcoind", + "-datadir="+tempBitcoindDir, + "-regtest", + "-connect="+miningNode.P2PAddress(), + "-txindex", + "-rpcauth=weks:469e9bb14ab2360f8e226efed5ca6f"+ + "d$507c670e800a95284294edb5773b05544b"+ + "220110063096c221be9933c82d38e1", + fmt.Sprintf("-rpcport=%d", rpcPort), + "-disablewallet", + "-zmqpubrawblock="+zmqPath, + "-zmqpubrawtx="+zmqPath, + ) + err = bitcoind.Start() + if err != nil { + t.Fatalf("couldn't start bitcoind: %v", err) + } + defer bitcoind.Wait() + defer bitcoind.Process.Kill() + + // Start an Alice btcwallet bitcoind back end instance. + aliceClient, err = chain.NewBitcoindClient(netParams, + fmt.Sprintf("127.0.0.1:%d", rpcPort), "weks", + "weks", zmqPath, 100*time.Millisecond) + if err != nil { + t.Fatalf("couldn't start alice client: %v", err) + } + + // Start a Bob btcwallet bitcoind back end instance. + bobClient, err = chain.NewBitcoindClient(netParams, + fmt.Sprintf("127.0.0.1:%d", rpcPort), "weks", + "weks", zmqPath, 100*time.Millisecond) + if err != nil { + t.Fatalf("couldn't start bob client: %v", err) } - bobSigner = bobWalletController.(*btcwallet.BtcWallet) - bio = bobWalletController.(*btcwallet.BtcWallet) default: - // TODO(roasbeef): add neutrino case - t.Fatalf("unknown wallet driver: %v", walletType) + t.Fatalf("unknown chain driver: %v", backEnd) } - // Funding via 20 outputs with 4BTC each. - alice, err := createTestWallet(tempTestDirAlice, miningNode, - netParams, chainNotifier, aliceWalletController, - aliceSigner, bio) + aliceWalletConfig := &btcwallet.Config{ + PrivatePass: []byte("alice-pass"), + HdSeed: aliceHDSeed[:], + DataDir: tempTestDirAlice, + NetParams: netParams, + ChainSource: aliceClient, + FeeEstimator: feeEstimator, + } + aliceWalletController, err = walletDriver.New(aliceWalletConfig) if err != nil { - t.Fatalf("unable to create test ln wallet: %v", err) + t.Fatalf("unable to create btcwallet: %v", err) } - defer alice.Shutdown() + aliceSigner = aliceWalletController.(*btcwallet.BtcWallet) - bob, err := createTestWallet(tempTestDirBob, miningNode, - netParams, chainNotifier, bobWalletController, - bobSigner, bio) + bobWalletConfig := &btcwallet.Config{ + PrivatePass: []byte("bob-pass"), + HdSeed: bobHDSeed[:], + DataDir: tempTestDirBob, + NetParams: netParams, + ChainSource: bobClient, + FeeEstimator: feeEstimator, + } + bobWalletController, err = walletDriver.New(bobWalletConfig) if err != nil { - t.Fatalf("unable to create test ln wallet: %v", err) + t.Fatalf("unable to create btcwallet: %v", err) } - defer bob.Shutdown() + bobSigner = bobWalletController.(*btcwallet.BtcWallet) + bio = bobWalletController.(*btcwallet.BtcWallet) + default: + t.Fatalf("unknown wallet driver: %v", walletType) + } - // Both wallets should now have 80BTC available for spending. - assertProperBalance(t, alice, 1, 80) - assertProperBalance(t, bob, 1, 80) + // Funding via 20 outputs with 4BTC each. + alice, err := createTestWallet(tempTestDirAlice, miningNode, netParams, + chainNotifier, aliceWalletController, aliceSigner, bio) + if err != nil { + t.Fatalf("unable to create test ln wallet: %v", err) + } + defer alice.Shutdown() - // Execute every test, clearing possibly mutated wallet state - // after each step. - for _, walletTest := range walletTests { - testName := fmt.Sprintf("%v:%v", walletType, - walletTest.name) - success := t.Run(testName, func(t *testing.T) { - walletTest.test(miningNode, alice, bob, t) - }) - if !success { - break - } + bob, err := createTestWallet(tempTestDirBob, miningNode, netParams, + chainNotifier, bobWalletController, bobSigner, bio) + if err != nil { + t.Fatalf("unable to create test ln wallet: %v", err) + } + defer bob.Shutdown() - // TODO(roasbeef): possible reset mining node's - // chainstate to initial level, cleanly wipe buckets - if err := clearWalletStates(alice, bob); err != nil && - err != bolt.ErrBucketNotFound { - t.Fatalf("unable to wipe wallet state: %v", err) - } + // Both wallets should now have 80BTC available for + // spending. + assertProperBalance(t, alice, 1, 80) + assertProperBalance(t, bob, 1, 80) + + // Execute every test, clearing possibly mutated + // wallet state after each step. + for _, walletTest := range walletTests { + testName := fmt.Sprintf("%v/%v:%v", walletType, backEnd, + walletTest.name) + success := t.Run(testName, func(t *testing.T) { + walletTest.test(miningNode, alice, bob, t) + }) + if !success { + break + } + + // TODO(roasbeef): possible reset mining + // node's chainstate to initial level, cleanly + // wipe buckets + if err := clearWalletStates(alice, bob); err != + nil && err != bolt.ErrBucketNotFound { + t.Fatalf("unable to wipe wallet state: %v", err) } } } diff --git a/routing/chainview/bitcoind.go b/routing/chainview/bitcoind.go new file mode 100644 index 00000000..6ec0b43f --- /dev/null +++ b/routing/chainview/bitcoind.go @@ -0,0 +1,460 @@ +package chainview + +import ( + "bytes" + "encoding/hex" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/roasbeef/btcd/btcjson" + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcd/chaincfg/chainhash" + "github.com/roasbeef/btcd/rpcclient" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" + "github.com/roasbeef/btcwallet/chain" + "github.com/roasbeef/btcwallet/wtxmgr" +) + +// BitcoindFilteredChainView is an implementation of the FilteredChainView +// interface which is backed by bitcoind. +type BitcoindFilteredChainView struct { + started int32 + stopped int32 + + // bestHeight is the height of the latest block added to the + // blockQueue from the onFilteredConnectedMethod. It is used to + // determine up to what height we would need to rescan in case + // of a filter update. + bestHeightMtx sync.Mutex + bestHeight uint32 + + // TODO: Factor out common logic between bitcoind and btcd into a + // NodeFilteredView interface. + chainClient *chain.BitcoindClient + + // blockEventQueue is the ordered queue used to keep the order + // of connected and disconnected blocks sent to the reader of the + // chainView. + blockQueue *blockEventQueue + + // filterUpdates is a channel in which updates to the utxo filter + // attached to this instance are sent over. + filterUpdates chan filterUpdate + + // chainFilter is the set of utox's that we're currently watching + // spends for within the chain. + filterMtx sync.RWMutex + chainFilter map[wire.OutPoint]struct{} + + // filterBlockReqs is a channel in which requests to filter select + // blocks will be sent over. + filterBlockReqs chan *filterBlockReq + + quit chan struct{} + wg sync.WaitGroup +} + +// A compile time check to ensure BitcoindFilteredChainView implements the +// chainview.FilteredChainView. +var _ FilteredChainView = (*BitcoindFilteredChainView)(nil) + +// NewBitcoindFilteredChainView creates a new instance of a FilteredChainView +// from RPC credentials and a ZMQ socket address for a bitcoind instance. +func NewBitcoindFilteredChainView(config rpcclient.ConnConfig, + zmqConnect string, params chaincfg.Params) (*BitcoindFilteredChainView, + error) { + chainView := &BitcoindFilteredChainView{ + chainFilter: make(map[wire.OutPoint]struct{}), + filterUpdates: make(chan filterUpdate), + filterBlockReqs: make(chan *filterBlockReq), + quit: make(chan struct{}), + } + + chainConn, err := chain.NewBitcoindClient(¶ms, config.Host, + config.User, config.Pass, zmqConnect, 100*time.Millisecond) + if err != nil { + return nil, err + } + chainView.chainClient = chainConn + + chainView.blockQueue = newBlockEventQueue() + + return chainView, nil +} + +// Start starts all goroutines necessary for normal operation. +// +// NOTE: This is part of the FilteredChainView interface. +func (b *BitcoindFilteredChainView) Start() error { + // Already started? + if atomic.AddInt32(&b.started, 1) != 1 { + return nil + } + + log.Infof("FilteredChainView starting") + + err := b.chainClient.Start() + if err != nil { + return err + } + + _, bestHeight, err := b.chainClient.GetBestBlock() + if err != nil { + return err + } + + b.bestHeightMtx.Lock() + b.bestHeight = uint32(bestHeight) + b.bestHeightMtx.Unlock() + + b.blockQueue.Start() + + b.wg.Add(1) + go b.chainFilterer() + + return nil +} + +// Stop stops all goroutines which we launched by the prior call to the Start +// method. +// +// NOTE: This is part of the FilteredChainView interface. +func (b *BitcoindFilteredChainView) Stop() error { + // Already shutting down? + if atomic.AddInt32(&b.stopped, 1) != 1 { + return nil + } + + // Shutdown the rpc client, this gracefully disconnects from bitcoind's + // zmq socket, and cleans up all related resources. + b.chainClient.Stop() + + b.blockQueue.Stop() + + log.Infof("FilteredChainView stopping") + + close(b.quit) + b.wg.Wait() + + return nil +} + +// onFilteredBlockConnected is called for each block that's connected to the +// end of the main chain. Based on our current chain filter, the block may or +// may not include any relevant transactions. +func (b *BitcoindFilteredChainView) onFilteredBlockConnected(height int32, + hash chainhash.Hash, txns []*wtxmgr.TxRecord) { + + mtxs := make([]*wire.MsgTx, len(txns)) + for i, tx := range txns { + mtxs[i] = &tx.MsgTx + + for _, txIn := range mtxs[i].TxIn { + // We can delete this outpoint from the chainFilter, as + // we just received a block where it was spent. In case + // of a reorg, this outpoint might get "un-spent", but + // that's okay since it would never be wise to consider + // the channel open again (since a spending transaction + // exists on the network). + b.filterMtx.Lock() + delete(b.chainFilter, txIn.PreviousOutPoint) + b.filterMtx.Unlock() + } + + } + + // We record the height of the last connected block added to the + // blockQueue such that we can scan up to this height in case of + // a rescan. It must be protected by a mutex since a filter update + // might be trying to read it concurrently. + b.bestHeightMtx.Lock() + b.bestHeight = uint32(height) + b.bestHeightMtx.Unlock() + + block := &FilteredBlock{ + Hash: hash, + Height: uint32(height), + Transactions: mtxs, + } + + b.blockQueue.Add(&blockEvent{ + eventType: connected, + block: block, + }) +} + +// onFilteredBlockDisconnected is a callback which is executed once a block is +// disconnected from the end of the main chain. +func (b *BitcoindFilteredChainView) onFilteredBlockDisconnected(height int32, + hash chainhash.Hash) { + + log.Debugf("got disconnected block at height %d: %v", height, + hash) + + filteredBlock := &FilteredBlock{ + Hash: hash, + Height: uint32(height), + } + + b.blockQueue.Add(&blockEvent{ + eventType: disconnected, + block: filteredBlock, + }) +} + +// FilterBlock takes a block hash, and returns a FilteredBlocks which is the +// result of applying the current registered UTXO sub-set on the block +// corresponding to that block hash. If any watched UTOX's are spent by the +// selected lock, then the internal chainFilter will also be updated. +// +// NOTE: This is part of the FilteredChainView interface. +func (b *BitcoindFilteredChainView) FilterBlock(blockHash *chainhash.Hash) (*FilteredBlock, error) { + req := &filterBlockReq{ + blockHash: blockHash, + resp: make(chan *FilteredBlock, 1), + err: make(chan error, 1), + } + + select { + case b.filterBlockReqs <- req: + case <-b.quit: + return nil, fmt.Errorf("FilteredChainView shutting down") + } + + return <-req.resp, <-req.err +} + +// chainFilterer is the primary goroutine which: listens for new blocks coming +// and dispatches the relevent FilteredBlock notifications, updates the filter +// due to requests by callers, and finally is able to preform targeted block +// filtration. +// +// TODO(roasbeef): change to use loadfilter RPC's +func (b *BitcoindFilteredChainView) chainFilterer() { + defer b.wg.Done() + + // filterBlock is a helper funciton that scans the given block, and + // notes which transactions spend outputs which are currently being + // watched. Additionally, the chain filter will also be updated by + // removing any spent outputs. + filterBlock := func(blk *wire.MsgBlock) []*wire.MsgTx { + var filteredTxns []*wire.MsgTx + for _, tx := range blk.Transactions { + for _, txIn := range tx.TxIn { + prevOp := txIn.PreviousOutPoint + if _, ok := b.chainFilter[prevOp]; ok { + filteredTxns = append(filteredTxns, tx) + + b.filterMtx.Lock() + delete(b.chainFilter, prevOp) + b.filterMtx.Unlock() + + break + } + } + } + + return filteredTxns + } + + decodeJSONBlock := func(block *btcjson.RescannedBlock, + height uint32) (*FilteredBlock, error) { + hash, err := chainhash.NewHashFromStr(block.Hash) + if err != nil { + return nil, err + + } + txs := make([]*wire.MsgTx, 0, len(block.Transactions)) + for _, str := range block.Transactions { + b, err := hex.DecodeString(str) + if err != nil { + return nil, err + } + tx := &wire.MsgTx{} + err = tx.Deserialize(bytes.NewReader(b)) + if err != nil { + return nil, err + } + txs = append(txs, tx) + } + return &FilteredBlock{ + Hash: *hash, + Height: height, + Transactions: txs, + }, nil + } + + for { + select { + // The caller has just sent an update to the current chain + // filter, so we'll apply the update, possibly rewinding our + // state partially. + case update := <-b.filterUpdates: + + // First, we'll add all the new UTXO's to the set of + // watched UTXO's, eliminating any duplicates in the + // process. + log.Debugf("Updating chain filter with new UTXO's: %v", + update.newUtxos) + for _, newOp := range update.newUtxos { + b.filterMtx.Lock() + b.chainFilter[newOp] = struct{}{} + b.filterMtx.Unlock() + } + + // Apply the new TX filter to the chain client, which + // will cause all following notifications from and + // calls to it return blocks filtered with the new + // filter. + b.chainClient.LoadTxFilter(false, []btcutil.Address{}, + update.newUtxos) + + // All blocks gotten after we loaded the filter will + // have the filter applied, but we will need to rescan + // the blocks up to the height of the block we last + // added to the blockQueue. + b.bestHeightMtx.Lock() + bestHeight := b.bestHeight + b.bestHeightMtx.Unlock() + + // If the update height matches our best known height, + // then we don't need to do any rewinding. + if update.updateHeight == bestHeight { + continue + } + + // Otherwise, we'll rewind the state to ensure the + // caller doesn't miss any relevant notifications. + // Starting from the height _after_ the update height, + // we'll walk forwards, rescanning one block at a time + // with the chain client applying the newly loaded + // filter to each block. + for i := update.updateHeight + 1; i < bestHeight+1; i++ { + blockHash, err := b.chainClient.GetBlockHash(int64(i)) + if err != nil { + log.Warnf("Unable to get block hash "+ + "for block at height %d: %v", + i, err) + continue + } + + // To avoid dealing with the case where a reorg + // is happening while we rescan, we scan one + // block at a time, skipping blocks that might + // have gone missing. + rescanned, err := b.chainClient.RescanBlocks( + []chainhash.Hash{*blockHash}) + if err != nil { + log.Warnf("Unable to rescan block "+ + "with hash %v at height %d: %v", + blockHash, i, err) + continue + } + + // If no block was returned from the rescan, it + // means no matching transactions were found. + if len(rescanned) != 1 { + log.Tracef("rescan of block %v at "+ + "height=%d yielded no "+ + "transactions", blockHash, i) + continue + } + decoded, err := decodeJSONBlock( + &rescanned[0], i) + if err != nil { + log.Errorf("Unable to decode block: %v", + err) + continue + } + b.blockQueue.Add(&blockEvent{ + eventType: connected, + block: decoded, + }) + } + + // We've received a new request to manually filter a block. + case req := <-b.filterBlockReqs: + // First we'll fetch the block itself as well as some + // additional information including its height. + block, err := b.chainClient.GetBlock(req.blockHash) + if err != nil { + req.err <- err + req.resp <- nil + continue + } + header, err := b.chainClient.GetBlockHeaderVerbose( + req.blockHash) + if err != nil { + req.err <- err + req.resp <- nil + continue + } + + // Once we have this info, we can directly filter the + // block and dispatch the proper notification. + req.resp <- &FilteredBlock{ + Hash: *req.blockHash, + Height: uint32(header.Height), + Transactions: filterBlock(block), + } + req.err <- err + + // We've received a new event from the chain client. + case event := <-b.chainClient.Notifications(): + switch e := event.(type) { + case chain.FilteredBlockConnected: + b.onFilteredBlockConnected(e.Block.Height, + e.Block.Hash, e.RelevantTxs) + case chain.BlockDisconnected: + b.onFilteredBlockDisconnected(e.Height, e.Hash) + } + + case <-b.quit: + return + } + } +} + +// UpdateFilter updates the UTXO filter which is to be consulted when creating +// FilteredBlocks to be sent to subscribed clients. This method is cumulative +// meaning repeated calls to this method should _expand_ the size of the UTXO +// sub-set currently being watched. If the set updateHeight is _lower_ than +// the best known height of the implementation, then the state should be +// rewound to ensure all relevant notifications are dispatched. +// +// NOTE: This is part of the FilteredChainView interface. +func (b *BitcoindFilteredChainView) UpdateFilter(ops []wire.OutPoint, updateHeight uint32) error { + select { + + case b.filterUpdates <- filterUpdate{ + newUtxos: ops, + updateHeight: updateHeight, + }: + return nil + + case <-b.quit: + return fmt.Errorf("chain filter shutting down") + } +} + +// FilteredBlocks returns the channel that filtered blocks are to be sent over. +// Each time a block is connected to the end of a main chain, and appropriate +// FilteredBlock which contains the transactions which mutate our watched UTXO +// set is to be returned. +// +// NOTE: This is part of the FilteredChainView interface. +func (b *BitcoindFilteredChainView) FilteredBlocks() <-chan *FilteredBlock { + return b.blockQueue.newBlocks +} + +// DisconnectedBlocks returns a receive only channel which will be sent upon +// with the empty filtered blocks of blocks which are disconnected from the +// main chain in the case of a re-org. +// +// NOTE: This is part of the FilteredChainView interface. +func (b *BitcoindFilteredChainView) DisconnectedBlocks() <-chan *FilteredBlock { + return b.blockQueue.staleBlocks +} diff --git a/routing/chainview/interface_test.go b/routing/chainview/interface_test.go index d0b0da07..c8c03c7a 100644 --- a/routing/chainview/interface_test.go +++ b/routing/chainview/interface_test.go @@ -4,13 +4,16 @@ import ( "bytes" "fmt" "io/ioutil" + "math/rand" "os" + "os/exec" "path/filepath" "runtime" "testing" "time" "github.com/lightninglabs/neutrino" + "github.com/ltcsuite/ltcd/btcjson" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/chaincfg" "github.com/roasbeef/btcd/chaincfg/chainhash" @@ -25,7 +28,7 @@ import ( ) var ( - netParams = &chaincfg.SimNetParams + netParams = &chaincfg.RegressionNetParams testPrivKey = []byte{ 0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda, @@ -42,6 +45,39 @@ var ( testScript, _ = txscript.PayToAddrScript(testAddr) ) +func waitForMempoolTx(r *rpctest.Harness, txid *chainhash.Hash) error { + var found bool + var tx *btcutil.Tx + var err error + timeout := time.After(10 * time.Second) + for !found { + // Do a short wait + select { + case <-timeout: + return fmt.Errorf("timeout after 10s") + default: + } + time.Sleep(100 * time.Millisecond) + + // Check for the harness' knowledge of the txid + tx, err = r.Node.GetRawTransaction(txid) + if err != nil { + switch e := err.(type) { + case *btcjson.RPCError: + if e.Code == btcjson.ErrRPCNoTxInfo { + continue + } + default: + } + return err + } + if tx != nil && tx.MsgTx().TxHash() == *txid { + found = true + } + } + return nil +} + func getTestTXID(miner *rpctest.Harness) (*chainhash.Hash, error) { script, err := txscript.PayToAddrScript(testAddr) if err != nil { @@ -131,11 +167,19 @@ func testFilterBlockNotifications(node *rpctest.Harness, // private key that we generated above. txid1, err := getTestTXID(node) if err != nil { - t.Fatalf("unable to get test txid") + t.Fatalf("unable to get test txid: %v", err) + } + err = waitForMempoolTx(node, txid1) + if err != nil { + t.Fatalf("unable to get test txid in mempool: %v", err) } txid2, err := getTestTXID(node) if err != nil { - t.Fatalf("unable to get test txid") + t.Fatalf("unable to get test txid: %v", err) + } + err = waitForMempoolTx(node, txid2) + if err != nil { + t.Fatalf("unable to get test txid in mempool: %v", err) } blockChan := chainView.FilteredBlocks() @@ -218,6 +262,10 @@ func testFilterBlockNotifications(node *rpctest.Harness, if err != nil { t.Fatalf("unable to broadcast transaction: %v", err) } + err = waitForMempoolTx(node, spendTxid1) + if err != nil { + t.Fatalf("unable to get spending txid in mempool: %v", err) + } newBlockHashes, err = node.Node.Generate(1) if err != nil { t.Fatalf("unable to generate block: %v", err) @@ -240,6 +288,10 @@ func testFilterBlockNotifications(node *rpctest.Harness, if err != nil { t.Fatalf("unable to broadcast transaction: %v", err) } + err = waitForMempoolTx(node, spendTxid2) + if err != nil { + t.Fatalf("unable to get spending txid in mempool: %v", err) + } newBlockHashes, err = node.Node.Generate(1) if err != nil { t.Fatalf("unable to generate block: %v", err) @@ -264,6 +316,10 @@ func testUpdateFilterBackTrack(node *rpctest.Harness, if err != nil { t.Fatalf("unable to get test txid") } + err = waitForMempoolTx(node, txid) + if err != nil { + t.Fatalf("unable to get test txid in mempool: %v", err) + } // Next we'll mine a block confirming the output generated above. initBlockHashes, err := node.Node.Generate(1) @@ -306,6 +362,10 @@ func testUpdateFilterBackTrack(node *rpctest.Harness, if err != nil { t.Fatalf("unable to broadcast transaction: %v", err) } + err = waitForMempoolTx(node, spendTxid) + if err != nil { + t.Fatalf("unable to get spending txid in mempool: %v", err) + } newBlockHashes, err := node.Node.Generate(1) if err != nil { t.Fatalf("unable to generate block: %v", err) @@ -352,10 +412,18 @@ func testFilterSingleBlock(node *rpctest.Harness, chainView FilteredChainView, if err != nil { t.Fatalf("unable to get test txid") } + err = waitForMempoolTx(node, txid1) + if err != nil { + t.Fatalf("unable to get test txid in mempool: %v", err) + } txid2, err := getTestTXID(node) if err != nil { t.Fatalf("unable to get test txid") } + err = waitForMempoolTx(node, txid2) + if err != nil { + t.Fatalf("unable to get test txid in mempool: %v", err) + } blockChan := chainView.FilteredBlocks() @@ -671,7 +739,7 @@ var chainViewTests = []testCase{ test: testUpdateFilterBackTrack, }, { - name: "fitler single block", + name: "filter single block", test: testFilterSingleBlock, }, { @@ -684,6 +752,68 @@ var interfaceImpls = []struct { name string chainViewInit chainViewInitFunc }{ + { + name: "bitcoind_zmq", + chainViewInit: func(_ rpcclient.ConnConfig, p2pAddr string) (func(), FilteredChainView, error) { + // Start a bitcoind instance. + tempBitcoindDir, err := ioutil.TempDir("", "bitcoind") + if err != nil { + return nil, nil, err + } + zmqPath := "ipc:///" + tempBitcoindDir + "/weks.socket" + cleanUp1 := func() { + os.RemoveAll(tempBitcoindDir) + } + rpcPort := rand.Int()%(65536-1024) + 1024 + bitcoind := exec.Command( + "bitcoind", + "-datadir="+tempBitcoindDir, + "-regtest", + "-connect="+p2pAddr, + "-txindex", + "-rpcauth=weks:469e9bb14ab2360f8e226efed5ca6f"+ + "d$507c670e800a95284294edb5773b05544b"+ + "220110063096c221be9933c82d38e1", + fmt.Sprintf("-rpcport=%d", rpcPort), + "-disablewallet", + "-zmqpubrawblock="+zmqPath, + "-zmqpubrawtx="+zmqPath, + ) + err = bitcoind.Start() + if err != nil { + cleanUp1() + return nil, nil, err + } + cleanUp2 := func() { + bitcoind.Process.Kill() + bitcoind.Wait() + cleanUp1() + } + + // Wait for the bitcoind instance to start up. + time.Sleep(time.Second) + + // Start the FilteredChainView implementation instance. + config := rpcclient.ConnConfig{ + Host: fmt.Sprintf( + "127.0.0.1:%d", rpcPort), + User: "weks", + Pass: "weks", + DisableAutoReconnect: false, + DisableConnectOnNew: true, + DisableTLS: true, + HTTPPostMode: true, + } + + chainView, err := NewBitcoindFilteredChainView(config, + zmqPath, chaincfg.RegressionNetParams) + if err != nil { + cleanUp2() + return nil, nil, err + } + return cleanUp2, chainView, nil + }, + }, { name: "p2p_neutrino", chainViewInit: func(_ rpcclient.ConnConfig, p2pAddr string) (func(), FilteredChainView, error) { From 9a02884d0bd16bc36ad79b0e576a3a4604ebfc79 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 21 Dec 2017 22:23:24 -0700 Subject: [PATCH 31/39] main/lntest: factor out node config options, add options for bitcoind This commit factors out the btcd and ltcd options into their own sections similar to neutrino, and adds a bitcoind section as well. Now, you specify node options similarly to: --ltcd.rpchost=... or --btcd.rpcuser=... or --bitcoind.zmqpath=... For Bitcoin, you specify an alternate back-end to btcd as follows: --bitcoin.node=bitcoind or --bitcoin.node=neutrino You can also specify the default option: --bitcoin.node=btcd For Litecoin, only `btcd` mode is valid, and corresponds to the `ltcd` section. For example: --litecoin.node=btcd --ltcd.rpchost=... The new code also attempts to read the correct options and auth info from bitcoin.conf just as it does from btcd.conf/ltcd.conf. --- chainregistry.go | 150 +++++++++++++++++++--- config.go | 328 +++++++++++++++++++++++++++++++++++------------ glide.lock | 14 +- glide.yaml | 6 +- lnd.go | 2 +- lntest/node.go | 8 +- mock.go | 5 + 7 files changed, 398 insertions(+), 115 deletions(-) diff --git a/chainregistry.go b/chainregistry.go index 675ad5cb..160d91f2 100644 --- a/chainregistry.go +++ b/chainregistry.go @@ -4,14 +4,17 @@ import ( "encoding/hex" "fmt" "io/ioutil" + "net" "os" "path/filepath" + "strconv" "strings" "sync" "time" "github.com/lightninglabs/neutrino" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/chainntnfs/bitcoindnotify" "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify" "github.com/lightningnetwork/lnd/chainntnfs/neutrinonotify" "github.com/lightningnetwork/lnd/channeldb" @@ -134,15 +137,17 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB, } var ( - err error - cleanUp func() - rawRPCConn *chain.RPCClient + err error + cleanUp func() + btcdConn *chain.RPCClient + bitcoindConn *chain.BitcoindClient ) // If spv mode is active, then we'll be using a distinct set of // chainControl interfaces that interface directly with the p2p network // of the selected chain. - if cfg.NeutrinoMode.Active { + switch homeChainConfig.Node { + case "neutrino": // First we'll open the database file for neutrino, creating // the database if needed. dbName := filepath.Join(cfg.DataDir, "neutrino.db") @@ -189,21 +194,121 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB, cleanUp = func() { defer nodeDatabase.Close() } - } else { + case "bitcoind": + // Otherwise, we'll be speaking directly via RPC and ZMQ to a + // bitcoind node. If the specified host for the btcd/ltcd RPC + // server already has a port specified, then we use that + // directly. Otherwise, we assume the default port according to + // the selected chain parameters. + var bitcoindHost string + if strings.Contains(cfg.BitcoindMode.RPCHost, ":") { + bitcoindHost = cfg.BitcoindMode.RPCHost + } else { + // The RPC ports specified in chainparams.go assume + // btcd, which picks a different port so that btcwallet + // can use the same RPC port as bitcoind. We convert + // this back to the btcwallet/bitcoind port. + rpcPort, err := strconv.Atoi(activeNetParams.rpcPort) + if err != nil { + return nil, nil, err + } + rpcPort -= 2 + bitcoindHost = fmt.Sprintf("%v:%d", + cfg.BitcoindMode.RPCHost, rpcPort) + if cfg.Bitcoin.RegTest { + conn, err := net.Dial("tcp", bitcoindHost) + if err != nil || conn == nil { + rpcPort = 18443 + bitcoindHost = fmt.Sprintf("%v:%d", + cfg.BitcoindMode.RPCHost, + rpcPort) + } else { + conn.Close() + } + } + } + + bitcoindUser := cfg.BitcoindMode.RPCUser + bitcoindPass := cfg.BitcoindMode.RPCPass + rpcConfig := &rpcclient.ConnConfig{ + Host: bitcoindHost, + User: bitcoindUser, + Pass: bitcoindPass, + DisableConnectOnNew: true, + DisableAutoReconnect: false, + DisableTLS: true, + HTTPPostMode: true, + } + cc.chainNotifier, err = bitcoindnotify.New(rpcConfig, + cfg.BitcoindMode.ZMQPath, *activeNetParams.Params) + if err != nil { + return nil, nil, err + } + + // Next, we'll create an instance of the bitcoind chain view to + // be used within the routing layer. + cc.chainView, err = chainview.NewBitcoindFilteredChainView( + *rpcConfig, cfg.BitcoindMode.ZMQPath, + *activeNetParams.Params) + if err != nil { + srvrLog.Errorf("unable to create chain view: %v", err) + return nil, nil, err + } + + // Create a special rpc+ZMQ client for bitcoind which will be + // used by the wallet for notifications, calls, etc. + bitcoindConn, err = chain.NewBitcoindClient( + activeNetParams.Params, bitcoindHost, bitcoindUser, + bitcoindPass, cfg.BitcoindMode.ZMQPath, + time.Millisecond*100) + if err != nil { + return nil, nil, err + } + + walletConfig.ChainSource = bitcoindConn + + // If we're not in regtest mode, then we'll attempt to use a + // proper fee estimator for testnet. + if !cfg.Bitcoin.RegTest { + ltndLog.Infof("Initializing bitcoind backed fee estimator") + + // Finally, we'll re-initialize the fee estimator, as + // if we're using bitcoind as a backend, then we can + // use live fee estimates, rather than a statically + // coded value. + fallBackFeeRate := btcutil.Amount(25) + cc.feeEstimator, err = lnwallet.NewBitcoindFeeEstimator( + *rpcConfig, fallBackFeeRate, + ) + if err != nil { + return nil, nil, err + } + if err := cc.feeEstimator.Start(); err != nil { + return nil, nil, err + } + } + case "btcd": // Otherwise, we'll be speaking directly via RPC to a node. // // So first we'll load btcd/ltcd's TLS cert for the RPC // connection. If a raw cert was specified in the config, then // we'll set that directly. Otherwise, we attempt to read the // cert from the path specified in the config. + var btcdMode *btcdConfig + switch { + case cfg.Bitcoin.Active: + btcdMode = cfg.BtcdMode + case cfg.Litecoin.Active: + btcdMode = cfg.LtcdMode + } var rpcCert []byte - if homeChainConfig.RawRPCCert != "" { - rpcCert, err = hex.DecodeString(homeChainConfig.RawRPCCert) + if btcdMode.RawRPCCert != "" { + rpcCert, err = hex.DecodeString(btcdMode.RawRPCCert) if err != nil { return nil, nil, err } } else { - certFile, err := os.Open(homeChainConfig.RPCCert) + certFile, err := os.Open(btcdMode.RPCCert) if err != nil { return nil, nil, err } @@ -221,15 +326,15 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB, // we assume the default port according to the selected chain // parameters. var btcdHost string - if strings.Contains(homeChainConfig.RPCHost, ":") { - btcdHost = homeChainConfig.RPCHost + if strings.Contains(btcdMode.RPCHost, ":") { + btcdHost = btcdMode.RPCHost } else { - btcdHost = fmt.Sprintf("%v:%v", homeChainConfig.RPCHost, + btcdHost = fmt.Sprintf("%v:%v", btcdMode.RPCHost, activeNetParams.rpcPort) } - btcdUser := homeChainConfig.RPCUser - btcdPass := homeChainConfig.RPCPass + btcdUser := btcdMode.RPCUser + btcdPass := btcdMode.RPCPass rpcConfig := &rpcclient.ConnConfig{ Host: btcdHost, Endpoint: "ws", @@ -262,7 +367,7 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB, } walletConfig.ChainSource = chainRPC - rawRPCConn = chainRPC + btcdConn = chainRPC // If we're not in simnet or regtest mode, then we'll attempt // to use a proper fee estimator for testnet. @@ -286,6 +391,9 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB, return nil, nil, err } } + default: + return nil, nil, fmt.Errorf("unknown node type: %s", + homeChainConfig.Node) } wc, err := btcwallet.New(*walletConfig) @@ -327,7 +435,7 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB, // As a final check, if we're using the RPC backend, we'll ensure that // the btcd node has the txindex set. Atm, this is required in order to // properly perform historical confirmation+spend dispatches. - if !cfg.NeutrinoMode.Active { + if homeChainConfig.Node != "neutrino" { // In order to check to see if we have the txindex up to date // and active, we'll try to fetch the first transaction in the // latest block via the index. If this doesn't succeed, then we @@ -344,12 +452,18 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB, } firstTxHash := bestBlock.Transactions[0].TxHash() - _, err = rawRPCConn.GetRawTransaction(&firstTxHash) + switch homeChainConfig.Node { + case "btcd": + _, err = btcdConn.GetRawTransaction(&firstTxHash) + case "bitcoind": + _, err = bitcoindConn.GetRawTransactionVerbose(&firstTxHash) + } if err != nil { // If the node doesn't have the txindex set, then we'll // halt startup, as we can't proceed in this state. - return nil, nil, fmt.Errorf("btcd detected to not " + - "have --txindex active, cannot proceed") + return nil, nil, fmt.Errorf("%s detected to not "+ + "have --txindex active, cannot proceed", + homeChainConfig.Node) } } diff --git a/config.go b/config.go index 4ce84bed..3258512c 100644 --- a/config.go +++ b/config.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "net" "os" + "path" "path/filepath" "regexp" "sort" @@ -16,7 +17,7 @@ import ( "strings" "time" - flags "github.com/btcsuite/go-flags" + flags "github.com/jessevdk/go-flags" "github.com/lightningnetwork/lnd/brontide" "github.com/lightningnetwork/lnd/lnwire" "github.com/roasbeef/btcd/btcec" @@ -72,17 +73,15 @@ var ( ltcdHomeDir = btcutil.AppDataDir("ltcd", false) defaultLtcdRPCCertFile = filepath.Join(ltcdHomeDir, "rpc.cert") + + bitcoindHomeDir = btcutil.AppDataDir("bitcoin", false) ) type chainConfig struct { Active bool `long:"active" description:"If the chain should be active or not."` ChainDir string `long:"chaindir" description:"The directory to store the chain's data within."` - RPCHost string `long:"rpchost" description:"The daemon's rpc listening address. If a port is omitted, then the default port for the selected chain parameters will be used."` - RPCUser string `long:"rpcuser" description:"Username for RPC connections"` - RPCPass string `long:"rpcpass" default-mask:"-" description:"Password for RPC connections"` - RPCCert string `long:"rpccert" description:"File containing the daemon's certificate file"` - RawRPCCert string `long:"rawrpccert" description:"The raw bytes of the daemon's PEM-encoded certificate chain which will be used to authenticate the RPC connection."` + Node string `long:"node" description:"The blockchain interface to use." choice:"btcd" choice:"bitcoind" choice:"neutrino"` TestNet3 bool `long:"testnet" description:"Use the test network"` SimNet bool `long:"simnet" description:"Use the simulation test network"` @@ -97,7 +96,6 @@ type chainConfig struct { } type neutrinoConfig struct { - Active bool `long:"active" description:"If SPV mode should be active or not."` AddPeers []string `short:"a" long:"addpeer" description:"Add a peer to connect with at startup"` ConnectPeers []string `long:"connect" description:"Connect only to the specified peers at startup"` MaxPeers int `long:"maxpeers" description:"Max number of inbound and outbound peers"` @@ -105,6 +103,21 @@ type neutrinoConfig struct { BanThreshold uint32 `long:"banthreshold" description:"Maximum allowed ban score before disconnecting and banning misbehaving peers."` } +type btcdConfig struct { + RPCHost string `long:"rpchost" description:"The daemon's rpc listening address. If a port is omitted, then the default port for the selected chain parameters will be used."` + RPCUser string `long:"rpcuser" description:"Username for RPC connections"` + RPCPass string `long:"rpcpass" default-mask:"-" description:"Password for RPC connections"` + RPCCert string `long:"rpccert" description:"File containing the daemon's certificate file"` + RawRPCCert string `long:"rawrpccert" description:"The raw bytes of the daemon's PEM-encoded certificate chain which will be used to authenticate the RPC connection."` +} + +type bitcoindConfig struct { + RPCHost string `long:"rpchost" description:"The daemon's rpc listening address. If a port is omitted, then the default port for the selected chain parameters will be used."` + RPCUser string `long:"rpcuser" description:"Username for RPC connections"` + RPCPass string `long:"rpcpass" default-mask:"-" description:"Password for RPC connections"` + ZMQPath string `long:"zmqpath" description:"The path to the ZMQ socket providing at least raw blocks. Raw transactions can be handled as well."` +} + type autoPilotConfig struct { // TODO(roasbeef): add Active bool `long:"active" description:"If the autopilot agent should be active or not."` @@ -144,11 +157,14 @@ type config struct { HodlHTLC bool `long:"hodlhtlc" description:"Activate the hodl HTLC mode. With hodl HTLC mode, all incoming HTLCs will be accepted by the receiving node, but no attempt will be made to settle the payment with the sender."` MaxPendingChannels int `long:"maxpendingchannels" description:"The maximum number of incoming pending channels permitted per peer."` - Litecoin *chainConfig `group:"Litecoin" namespace:"litecoin"` - Bitcoin *chainConfig `group:"Bitcoin" namespace:"bitcoin"` - + Bitcoin *chainConfig `group:"Bitcoin" namespace:"bitcoin"` + BtcdMode *btcdConfig `group:"btcd" namespace:"btcd"` + BitcoindMode *bitcoindConfig `group:"bitcoind" namespace:"bitcoind"` NeutrinoMode *neutrinoConfig `group:"neutrino" namespace:"neutrino"` + Litecoin *chainConfig `group:"Litecoin" namespace:"litecoin"` + LtcdMode *btcdConfig `group:"ltcd" namespace:"ltcd"` + Autopilot *autoPilotConfig `group:"autopilot" namespace:"autopilot"` NoNetBootstrap bool `long:"nobootstrap" description:"If true, then automatic network bootstrapping will not be attempted."` @@ -168,35 +184,44 @@ type config struct { // 4) Parse CLI options and overwrite/add any specified options func loadConfig() (*config, error) { defaultCfg := config{ - ConfigFile: defaultConfigFile, - DataDir: defaultDataDir, - DebugLevel: defaultLogLevel, - TLSCertPath: defaultTLSCertPath, - TLSKeyPath: defaultTLSKeyPath, - AdminMacPath: defaultAdminMacPath, - ReadMacPath: defaultReadMacPath, - LogDir: defaultLogDir, - PeerPort: defaultPeerPort, - RPCPort: defaultRPCPort, - RESTPort: defaultRESTPort, - MaxPendingChannels: defaultMaxPendingChannels, - NoEncryptWallet: defaultNoEncryptWallet, + ConfigFile: defaultConfigFile, + DataDir: defaultDataDir, + DebugLevel: defaultLogLevel, + TLSCertPath: defaultTLSCertPath, + TLSKeyPath: defaultTLSKeyPath, + AdminMacPath: defaultAdminMacPath, + ReadMacPath: defaultReadMacPath, + LogDir: defaultLogDir, + PeerPort: defaultPeerPort, + RPCPort: defaultRPCPort, + RESTPort: defaultRESTPort, Bitcoin: &chainConfig{ - RPCHost: defaultRPCHost, - RPCCert: defaultBtcdRPCCertFile, MinHTLC: defaultBitcoinMinHTLCMSat, BaseFee: defaultBitcoinBaseFeeMSat, FeeRate: defaultBitcoinFeeRate, TimeLockDelta: defaultBitcoinTimeLockDelta, + Node: "btcd", + }, + BtcdMode: &btcdConfig{ + RPCHost: defaultRPCHost, + RPCCert: defaultBtcdRPCCertFile, + }, + BitcoindMode: &bitcoindConfig{ + RPCHost: defaultRPCHost, }, Litecoin: &chainConfig{ - RPCHost: defaultRPCHost, - RPCCert: defaultLtcdRPCCertFile, MinHTLC: defaultLitecoinMinHTLCMSat, BaseFee: defaultLitecoinBaseFeeMSat, FeeRate: defaultLitecoinFeeRate, TimeLockDelta: defaultLitecoinTimeLockDelta, + Node: "btcd", }, + LtcdMode: &btcdConfig{ + RPCHost: defaultRPCHost, + RPCCert: defaultLtcdRPCCertFile, + }, + MaxPendingChannels: defaultMaxPendingChannels, + NoEncryptWallet: defaultNoEncryptWallet, Autopilot: &autoPilotConfig{ MaxChannels: 5, Allocation: 0.6, @@ -252,28 +277,18 @@ func loadConfig() (*config, error) { return nil, err } + switch { // At this moment, multiple active chains are not supported. - if cfg.Litecoin.Active && cfg.Bitcoin.Active { + case cfg.Litecoin.Active && cfg.Bitcoin.Active: str := "%s: Currently both Bitcoin and Litecoin cannot be " + "active together" - err := fmt.Errorf(str, funcName) - return nil, err - } - - switch { - // The SPV mode implemented currently doesn't support Litecoin, so the - // two modes are incompatible. - case cfg.NeutrinoMode.Active && cfg.Litecoin.Active: - str := "%s: The light client mode currently supported does " + - "not yet support execution on the Litecoin network" - err := fmt.Errorf(str, funcName) - return nil, err + return nil, fmt.Errorf(str, funcName) // Either Bitcoin must be active, or Litecoin must be active. // Otherwise, we don't know which chain we're on. case !cfg.Bitcoin.Active && !cfg.Litecoin.Active: - return nil, fmt.Errorf("either bitcoin.active or " + - "litecoin.active must be set to 1 (true)") + return nil, fmt.Errorf("%s: either bitcoin.active or "+ + "litecoin.active must be set to 1 (true)", funcName) case cfg.Litecoin.Active: if cfg.Litecoin.SimNet { @@ -286,25 +301,26 @@ func loadConfig() (*config, error) { minTimeLockDelta) } + if cfg.Litecoin.Node != "btcd" { + str := "%s: only ltcd (`btcd`) mode supported for litecoin at this time" + return nil, fmt.Errorf(str, funcName) + } + // The litecoin chain is the current active chain. However - // throughout the codebase we required chiancfg.Params. So as a + // throughout the codebase we required chaincfg.Params. So as a // temporary hack, we'll mutate the default net params for // bitcoin with the litecoin specific information. paramCopy := bitcoinTestNetParams applyLitecoinParams(¶mCopy) activeNetParams = paramCopy - if !cfg.NeutrinoMode.Active { - // Attempt to parse out the RPC credentials for the - // litecoin chain if the information wasn't specified - err := parseRPCParams(cfg.Litecoin, litecoinChain, funcName) - if err != nil { - err := fmt.Errorf("unable to load RPC credentials for "+ - "ltcd: %v", err) - return nil, err - } + err := parseRPCParams(cfg.Litecoin, cfg.LtcdMode, litecoinChain, + funcName) + if err != nil { + err := fmt.Errorf("unable to load RPC credentials for "+ + "ltcd: %v", err) + return nil, err } - cfg.Litecoin.ChainDir = filepath.Join(cfg.DataDir, litecoinChain.String()) // Finally we'll register the litecoin chain as our current @@ -340,15 +356,29 @@ func loadConfig() (*config, error) { minTimeLockDelta) } - if !cfg.NeutrinoMode.Active { - // If needed, we'll attempt to automatically configure - // the RPC control plan for the target btcd node. - err := parseRPCParams(cfg.Bitcoin, bitcoinChain, funcName) + switch cfg.Bitcoin.Node { + case "btcd": + err := parseRPCParams(cfg.Bitcoin, cfg.BtcdMode, + bitcoinChain, funcName) if err != nil { - err := fmt.Errorf("unable to load RPC credentials for "+ - "btcd: %v", err) + err := fmt.Errorf("unable to load RPC "+ + "credentials for btcd: %v", err) return nil, err } + case "bitcoind": + if cfg.Bitcoin.SimNet { + return nil, fmt.Errorf("%s: bitcoind does not "+ + "support simnet", funcName) + } + err := parseRPCParams(cfg.Bitcoin, cfg.BitcoindMode, + bitcoinChain, funcName) + if err != nil { + err := fmt.Errorf("unable to load RPC "+ + "credentials for bitcoind: %v", err) + return nil, err + } + case "neutrino": + // No need to get RPC parameters. } cfg.Bitcoin.ChainDir = filepath.Join(cfg.DataDir, bitcoinChain.String()) @@ -539,12 +569,20 @@ func noiseDial(idPriv *btcec.PrivateKey) func(net.Addr) (net.Conn, error) { } } -func parseRPCParams(cConfig *chainConfig, net chainCode, funcName string) error { - // If the rpcuser and rpcpass parameters aren't set, then we'll attempt - // to automatically obtain the proper credentials for btcd and set - // them within the configuration. - if cConfig.RPCUser != "" || cConfig.RPCPass != "" { - return nil +func parseRPCParams(cConfig *chainConfig, nodeConfig interface{}, net chainCode, + funcName string) error { + // If the configuration has already set the RPCUser and RPCPass, and + // if we're either not using bitcoind mode or the ZMQ path is already + // specified, we can return. + switch conf := nodeConfig.(type) { + case *btcdConfig: + if conf.RPCUser != "" || conf.RPCPass != "" { + return nil + } + case *bitcoindConfig: + if conf.RPCUser != "" || conf.RPCPass != "" || conf.ZMQPath != "" { + return nil + } } // If we're in simnet mode, then the running btcd instance won't read @@ -556,33 +594,62 @@ func parseRPCParams(cConfig *chainConfig, net chainCode, funcName string) error return fmt.Errorf(str, funcName) } - daemonName := "btcd" - if net == litecoinChain { - daemonName = "ltcd" + var daemonName, homeDir, confFile string + switch net { + case bitcoinChain: + switch cConfig.Node { + case "btcd": + daemonName = "btcd" + homeDir = btcdHomeDir + confFile = "btcd" + case "bitcoind": + daemonName = "bitcoind" + homeDir = bitcoindHomeDir + confFile = "bitcoin" + } + case litecoinChain: + switch cConfig.Node { + case "btcd": + daemonName = "ltcd" + homeDir = ltcdHomeDir + confFile = "ltcd" + case "bitcoind": + return fmt.Errorf("bitcoind mode doesn't work with Litecoin yet") + } } + fmt.Println("Attempting automatic RPC configuration to " + daemonName) - homeDir := btcdHomeDir - if net == litecoinChain { - homeDir = ltcdHomeDir - } - confFile := filepath.Join(homeDir, fmt.Sprintf("%v.conf", daemonName)) - rpcUser, rpcPass, err := extractRPCParams(confFile) - if err != nil { - return fmt.Errorf("unable to extract RPC "+ - "credentials: %v, cannot start w/o RPC connection", - err) + confFile = filepath.Join(homeDir, fmt.Sprintf("%v.conf", confFile)) + switch cConfig.Node { + case "btcd": + nConf := nodeConfig.(*btcdConfig) + rpcUser, rpcPass, err := extractBtcdRPCParams(confFile) + if err != nil { + return fmt.Errorf("unable to extract RPC credentials:"+ + " %v, cannot start w/o RPC connection", + err) + } + nConf.RPCUser, nConf.RPCPass = rpcUser, rpcPass + case "bitcoind": + nConf := nodeConfig.(*bitcoindConfig) + rpcUser, rpcPass, zmqPath, err := extractBitcoindRPCParams(confFile) + if err != nil { + return fmt.Errorf("unable to extract RPC credentials:"+ + " %v, cannot start w/o RPC connection", + err) + } + nConf.RPCUser, nConf.RPCPass, nConf.ZMQPath = rpcUser, rpcPass, zmqPath } fmt.Printf("Automatically obtained %v's RPC credentials\n", daemonName) - cConfig.RPCUser, cConfig.RPCPass = rpcUser, rpcPass return nil } -// extractRPCParams attempts to extract the RPC credentials for an existing +// extractBtcdRPCParams attempts to extract the RPC credentials for an existing // btcd instance. The passed path is expected to be the location of btcd's // application data directory on the target system. -func extractRPCParams(btcdConfigPath string) (string, string, error) { +func extractBtcdRPCParams(btcdConfigPath string) (string, string, error) { // First, we'll open up the btcd configuration file found at the target // destination. btcdConfigFile, err := os.Open(btcdConfigPath) @@ -592,7 +659,7 @@ func extractRPCParams(btcdConfigPath string) (string, string, error) { defer btcdConfigFile.Close() // With the file open extract the contents of the configuration file so - // we can attempt o locate the RPC credentials. + // we can attempt to locate the RPC credentials. configContents, err := ioutil.ReadAll(btcdConfigFile) if err != nil { return "", "", err @@ -624,3 +691,100 @@ func extractRPCParams(btcdConfigPath string) (string, string, error) { return string(userSubmatches[1]), string(passSubmatches[1]), nil } + +// extractBitcoindParams attempts to extract the RPC credentials for an +// existing bitcoind node instance. The passed path is expected to be the +// location of bitcoind's bitcoin.conf on the target system. The routine looks +// for a cookie first, optionally following the datadir configuration option in +// the bitcoin.conf. If it doesn't find one, it looks for rpcuser/rpcpassword. +func extractBitcoindRPCParams(bitcoindConfigPath string) (string, string, string, error) { + + // First, we'll open up the bitcoind configuration file found at the + // target destination. + bitcoindConfigFile, err := os.Open(bitcoindConfigPath) + if err != nil { + return "", "", "", err + } + defer bitcoindConfigFile.Close() + + // With the file open extract the contents of the configuration file so + // we can attempt to locate the RPC credentials. + configContents, err := ioutil.ReadAll(bitcoindConfigFile) + if err != nil { + return "", "", "", err + } + + // First, we look for the ZMQ path for raw blocks. If raw transactions + // are sent over this interface, we can also get unconfirmed txs. + zmqPathRE, err := regexp.Compile(`(?m)^\s*zmqpubrawblock=([^\s]+)`) + if err != nil { + return "", "", "", err + } + zmqPathSubmatches := zmqPathRE.FindSubmatch(configContents) + if len(zmqPathSubmatches) < 2 { + return "", "", "", fmt.Errorf("unable to find zmqpubrawblock in config") + } + + // Next, we'll try to find an auth cookie. We need to detect the chain + // by seeing if one is specified in the configuration file. + dataDir := path.Dir(bitcoindConfigPath) + dataDirRE, err := regexp.Compile(`(?m)^\s*datadir=([^\s]+)`) + if err != nil { + return "", "", "", err + } + dataDirSubmatches := dataDirRE.FindSubmatch(configContents) + if dataDirSubmatches != nil { + dataDir = string(dataDirSubmatches[1]) + } + + chainDir := "/" + netRE, err := regexp.Compile(`(?m)^\s*(testnet|regtest)=[\d]+`) + if err != nil { + return "", "", "", err + } + netSubmatches := netRE.FindSubmatch(configContents) + if netSubmatches != nil { + switch string(netSubmatches[1]) { + case "testnet": + chainDir = "/testnet3/" + case "regtest": + chainDir = "/regtest/" + } + } + + cookie, err := ioutil.ReadFile(dataDir + chainDir + ".cookie") + if err == nil { + splitCookie := strings.Split(string(cookie), ":") + if len(splitCookie) == 2 { + return splitCookie[0], splitCookie[1], + string(zmqPathSubmatches[1]), nil + } + } + + // We didn't find a cookie, so we attempt to locate the RPC user using + // a regular expression. If we don't have a match for our regular + // expression then we'll exit with an error. + rpcUserRegexp, err := regexp.Compile(`(?m)^\s*rpcuser=([^\s]+)`) + if err != nil { + return "", "", "", err + } + userSubmatches := rpcUserRegexp.FindSubmatch(configContents) + if userSubmatches == nil { + return "", "", "", fmt.Errorf("unable to find rpcuser in config") + } + + // Similarly, we'll use another regular expression to find the set + // rpcpass (if any). If we can't find the pass, then we'll exit with an + // error. + rpcPassRegexp, err := regexp.Compile(`(?m)^\s*rpcpassword=([^\s]+)`) + if err != nil { + return "", "", "", err + } + passSubmatches := rpcPassRegexp.FindSubmatch(configContents) + if passSubmatches == nil { + return "", "", "", fmt.Errorf("unable to find rpcpassword in config") + } + + return string(userSubmatches[1]), string(passSubmatches[1]), + string(zmqPathSubmatches[1]), nil +} diff --git a/glide.lock b/glide.lock index b6721ecc..373c9d12 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: d145c16f2f9cfdf4937eb8b7cdd65919a8c351593a179acc23f2cbca5b42f34b -updated: 2017-12-22T23:45:25.148488338-07:00 +hash: 59d73259b4445fdb5982f2ec2538f53805e6e789c26b98b0fdfef43b7d0676a4 +updated: 2018-01-12T11:04:32.444091731-07:00 imports: - name: github.com/aead/chacha20 version: d31a916ded42d1640b9d89a26f8abd53cc96790c @@ -21,8 +21,8 @@ imports: version: 84c8d2346e9fc8c7b947e243b9c24e6df9fd206a - name: github.com/btcsuite/fastsha256 version: 637e656429416087660c84436a2a035d69d54e2e -- name: github.com/btcsuite/go-flags - version: 6c288d648c1cc1befcb90cb5511dcacf64ae8e61 +- name: github.com/jessevdk/go-flags + version: f88afde2fa19a30cf50ba4b05b3d13bc6bae3079 - name: github.com/btcsuite/go-socks version: 4720035b7bfd2a9bb130b1c184f8bbe41b6f0d0f subpackages: @@ -86,8 +86,8 @@ imports: version: 946bd9fbed05568b0f3cd188353d8aa28f38b688 subpackages: - internal/socket -- name: github.com/pebbe/zmq4 - version: 90d69e412a09549f2e90bac70fbb449081f1e5c1 +- name: github.com/lightninglabs/gozmq + version: b0bbb30a99d00b25c3304994d20aac68607b75c0 - name: github.com/roasbeef/btcd version: 9978b939c33973be19b932fa7b936079bb7ba38d subpackages: @@ -118,7 +118,7 @@ imports: - hdkeychain - txsort - name: github.com/roasbeef/btcwallet - version: 3037a033935cca719ba3c472870e7d080443a82f + version: 6c1491e092f39d6ed4e9103b9dc8322d04ca98bb subpackages: - chain - internal/helpers diff --git a/glide.yaml b/glide.yaml index 0174fc59..6cd704bf 100644 --- a/glide.yaml +++ b/glide.yaml @@ -4,7 +4,7 @@ import: version: ^1.2.1 - package: github.com/btcsuite/btclog version: 84c8d2346e9fc8c7b947e243b9c24e6df9fd206a -- package: github.com/btcsuite/go-flags +- package: github.com/jessevdk/go-flags - package: github.com/jrick/logrotate version: a93b200c26cbae3bb09dd0dc2c7c7fe1468a034a - package: github.com/davecgh/go-spew @@ -24,7 +24,7 @@ import: - txscript - wire - connmgr -- package: github.com/pebbe/zmq4 +- package: github.com/lightninglabs/gozmq - package: github.com/roasbeef/btcrpcclient version: d0f4db8b4dad0ca3d569b804f21247c3dd96acbb - package: github.com/roasbeef/btcutil @@ -35,7 +35,7 @@ import: - hdkeychain - txsort - package: github.com/roasbeef/btcwallet - version: 3037a033935cca719ba3c472870e7d080443a82f + version: 6c1491e092f39d6ed4e9103b9dc8322d04ca98bb subpackages: - chain - waddrmgr diff --git a/lnd.go b/lnd.go index af9e0bb4..46e281ac 100644 --- a/lnd.go +++ b/lnd.go @@ -31,8 +31,8 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials" - flags "github.com/btcsuite/go-flags" proxy "github.com/grpc-ecosystem/grpc-gateway/runtime" + flags "github.com/jessevdk/go-flags" "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lnrpc" diff --git a/lntest/node.go b/lntest/node.go index 43f93292..37af9168 100644 --- a/lntest/node.go +++ b/lntest/node.go @@ -124,10 +124,10 @@ func (cfg nodeConfig) genArgs() []string { args = append(args, "--debuglevel=debug") args = append(args, "--bitcoin.defaultchanconfs=1") args = append(args, "--bitcoin.defaultremotedelay=4") - args = append(args, fmt.Sprintf("--bitcoin.rpchost=%v", cfg.RPCConfig.Host)) - args = append(args, fmt.Sprintf("--bitcoin.rpcuser=%v", cfg.RPCConfig.User)) - args = append(args, fmt.Sprintf("--bitcoin.rpcpass=%v", cfg.RPCConfig.Pass)) - args = append(args, fmt.Sprintf("--bitcoin.rawrpccert=%v", encodedCert)) + args = append(args, fmt.Sprintf("--btcd.rpchost=%v", cfg.RPCConfig.Host)) + args = append(args, fmt.Sprintf("--btcd.rpcuser=%v", cfg.RPCConfig.User)) + args = append(args, fmt.Sprintf("--btcd.rpcpass=%v", cfg.RPCConfig.Pass)) + args = append(args, fmt.Sprintf("--btcd.rawrpccert=%v", encodedCert)) args = append(args, fmt.Sprintf("--rpcport=%v", cfg.RPCPort)) args = append(args, fmt.Sprintf("--peerport=%v", cfg.P2PPort)) args = append(args, fmt.Sprintf("--logdir=%v", cfg.LogDir)) diff --git a/mock.go b/mock.go index 3a846f6a..ec5e0573 100644 --- a/mock.go +++ b/mock.go @@ -183,6 +183,11 @@ type mockWalletController struct { publishedTransactions chan *wire.MsgTx } +// BackEnd returns "mock" to signify a mock wallet controller. +func (*mockWalletController) BackEnd() string { + return "mock" +} + // FetchInputInfo will be called to get info about the inputs to the funding // transaction. func (*mockWalletController) FetchInputInfo( From db55569bac48fe12ea99d71d92cf3d0c30f60b79 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 10 Jan 2018 17:19:43 -0700 Subject: [PATCH 32/39] chainntfns: stop neutrino service before closing DB in interface_test.go --- chainntnfs/interface_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainntnfs/interface_test.go b/chainntnfs/interface_test.go index 1a1635dc..04e1127a 100644 --- a/chainntnfs/interface_test.go +++ b/chainntnfs/interface_test.go @@ -1423,8 +1423,8 @@ func TestInterfaces(t *testing.T) { spvNode.Start() cleanUp = func() { - spvDatabase.Close() spvNode.Stop() + spvDatabase.Close() os.RemoveAll(spvDir) } From 1305a4fedfed3c5dbc9ed52596b3d31386e062df Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 12 Jan 2018 11:47:48 -0700 Subject: [PATCH 33/39] build: update travis to install bitcoind for running tests --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 07169dbb..46c1a456 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,9 @@ go: - 1.9.2 sudo: false install: + - add-apt-repository -y ppa:bitcoin/bitcoin + - apt-update + - apt-get -y install bitcoind - GLIDE_TAG=v0.12.3 - GLIDE_DOWNLOAD="https://github.com/Masterminds/glide/releases/download/$GLIDE_TAG/glide-$GLIDE_TAG-linux-amd64.tar.gz" - curl -L $GLIDE_DOWNLOAD | tar -xvz From 3da0e2011b64e6ca18a800bf8c871d6bdd976c08 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 12 Jan 2018 22:34:46 -0700 Subject: [PATCH 34/39] docker, docs, sample-lnd.conf: update examples for new command-line options --- docker/lnd/start-lnd.sh | 13 ++-- docs/INSTALL.md | 25 +++++-- sample-lnd.conf | 153 +++++++++++++++++++++++++--------------- 3 files changed, 128 insertions(+), 63 deletions(-) diff --git a/docker/lnd/start-lnd.sh b/docker/lnd/start-lnd.sh index 2601d8a0..e3367d9a 100755 --- a/docker/lnd/start-lnd.sh +++ b/docker/lnd/start-lnd.sh @@ -44,15 +44,20 @@ RPCPASS=$(set_default "$RPCPASS" "devpass") DEBUG=$(set_default "$DEBUG" "debug") NETWORK=$(set_default "$NETWORK" "simnet") CHAIN=$(set_default "$CHAIN" "bitcoin") +BACKEND="btcd" +if [[ "$CHAIN" == "litecoin" ]] + BACKEND="ltcd +fi lnd \ --noencryptwallet \ --logdir="/data" \ - "--$CHAIN.rpccert"="/rpc/rpc.cert" \ "--$CHAIN.active" \ "--$CHAIN.$NETWORK" \ - "--$CHAIN.rpchost"="blockchain" \ - "--$CHAIN.rpcuser"="$RPCUSER" \ - "--$CHAIN.rpcpass"="$RPCPASS" \ + "--$CHAIN.node"="btcd" \ + "--$BACKEND.rpccert"="/rpc/rpc.cert" \ + "--$BACKEND.rpchost"="blockchain" \ + "--$BACKEND.rpcuser"="$RPCUSER" \ + "--$BACKEND.rpcpass"="$RPCPASS" \ --debuglevel="$DEBUG" \ "$@" diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 42a1b0ac..b58da946 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -164,7 +164,7 @@ To run lnd in neutrino mode, run `lnd` with the following arguments, (swapping in `--bitcoin.simnet` for `simnet` mode if needed), and also your own `btcd` node if available: ``` -lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug --neutrino.active --neutrino.connect=faucet.lightning.community +lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug --bitcoin.node=neutrino --neutrino.connect=faucet.lightning.community ``` #### Running lnd using the btcd backend @@ -174,7 +174,22 @@ Otherwise, replace `--bitcoin.testnet` with `--bitcoin.simnet`. If you are installing `lnd` in preparation for the [tutorial](http://dev.lightning.community/tutorial), you may skip this step. ``` -lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug --bitcoin.rpcuser=kek --bitcoin.rpcpass=kek --externalip=X.X.X.X +lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug --btcd.rpcuser=kek --btcd.rpcpass=kek --externalip=X.X.X.X +``` + +#### Running lnd using the bitcoind backend + +In order to run `lnd` with a `bitcoind` back-end, the `bitcoind` instance must +be configured with `--txindex` just like `btcd` above. In addition, you'll need + to configure the `bitcoind` instance with `--zmqpubrawblock` and `--zmqpubrawtx` +(the latter is optional but allows you to see unconfirmed transactions in your +wallet). They must be combined in the same ZMQ socket address. Then, run this +command after `bitcoind` has finished syncing on testnet. Otherwise, replace +`--bitcoin.testnet` with `--bitcoin.regtest`. Please note that the `rpcuser` +and `rpcpass` parameters can typically be determined by `lnd` for a `bitcoind` +instance running under the same user, including when using cookie auth. +``` +lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug --bitcoin.node=bitcoind --bitcoind.rpcuser=kek bitcoind.rpcpass=kek --externalip=X.X.X.X ``` #### Network Reachability @@ -196,7 +211,7 @@ at the command line, you can create an `lnd.conf`. **On Linux, located at:** `~/.lnd/lnd.conf` -Here's a sample `lnd.conf` to get you started: +Here's a sample `lnd.conf` for `btcd` to get you started: ``` [Application Options] debuglevel=trace @@ -210,7 +225,9 @@ bitcoin.active=1 Notice the `[Bitcoin]` section. This section houses the parameters for the Bitcoin chain. `lnd` also supports Litecoin testnet4 (but not both BTC and LTC at the same time), so when working with Litecoin be sure to set to parameters -for Litecoin accordingly. +for Litecoin accordingly. For node configuration, the sections are called +`[Btcd]`, `[Bitcoind]`, `[Neutrino]`, and `[Ltcd]` depending on which chain +and node type you're using. # Accurate as of: - _roasbeef/btcd commit:_ `f8c02aff4e7a807ba0c1349e2db03695d8e790e8` diff --git a/sample-lnd.conf b/sample-lnd.conf index 6bd3fd8c..7e1398cc 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -97,29 +97,6 @@ ; active. bitcoin.active=1 -; The host that your local btcd daemon is listening on. This MUST be set if -; neutrino mode isn't active. -; bitcoin.rpchost=localhost - -; Username for RPC connections to btcd. This only needs to be set if neutrino -; mode isn't active. By default, lnd will attempt to automatically obtain the -; credentials, so this likely won't need to be set (other than for simnet mode). -; bitcoin.rpcuser=kek - -; Password for RPC connections to btcd. This only needs to be set if neutrino -; mode isn't active. By default, lnd will attempt to automatically obtain the -; credentials, so this likely won't need to be set (other than for simnet mode). -; bitcoin.rpcpass=kek - -; File containing the daemon's certificate file. This only needs to be set if -; the node isn't on the same host as lnd. -; bitcoin.rpccert=~/.btcd/rpc.cert - -; The raw bytes of the daemon's PEM-encoded certificate chain which will be used -; to authenticate the RPC connection. This only needs to be set if the btcd -; node is on a remote host. -; bitcoin.rawrpccert= - ; Use Bitcoin's test network. ; bitcoin.testnet=1 ; @@ -129,6 +106,77 @@ bitcoin.simnet=1 ; Use Bitcoin's regression test network ; bitcoin.regtest=false +; Use the btcd back-end +bitcoin.node=btcd + +; Use the bitcoind back-end +; bitcoin.node=bitcoind + +; Use the neutrino (light client) back-end +; bitcoin.node=neutrino + + +[Btcd] + +; The host that your local btcd daemon is listening on. By default, this +; setting is assumed to be localhost with the default port for the current +; network. +; btcd.rpchost=localhost + +; Username for RPC connections to btcd. By default, lnd will attempt to +; automatically obtain the credentials, so this likely won't need to be set +; (other than for simnet mode). +; btcd.rpcuser=kek + +; Password for RPC connections to btcd. By default, lnd will attempt to +; automatically obtain the credentials, so this likely won't need to be set +; (other than for simnet mode). +; btcd.rpcpass=kek + +; File containing the daemon's certificate file. This only needs to be set if +; the node isn't on the same host as lnd. +; btcd.rpccert=~/.btcd/rpc.cert + +; The raw bytes of the daemon's PEM-encoded certificate chain which will be used +; to authenticate the RPC connection. This only needs to be set if the btcd +; node is on a remote host. +; btcd.rawrpccert= + + +[Bitcoind] + +; The host that your local bitcoind daemon is listening on. By default, this +; setting is assumed to be localhost with the default port for the current +; network. +; bitcoind.rpchost=localhost + +; Username for RPC connections to bitcoind. By default, lnd will attempt to +; automatically obtain the credentials, so this likely won't need to be set +; (other than for a remote bitcoind instance). +; bitcoind.rpcuser=kek + +; Password for RPC connections to bitcoind. By default, lnd will attempt to +; automatically obtain the credentials, so this likely won't need to be set +; (other than for a remote bitcoind instance). +; bitcoind.rpcpass=kek + +; ZMQ socket which sends rawblock and (optionally) rawtx notifications from +; bitcoind. By default, lnd will attempt to automatically obtain this +; information, so this likely won't need to be set (other than for a remote +; bitcoind instance). +; bitcoind.zmqpath=tcp://127.0.0.1:18501 + + +[neutrino] + +; Connect only to the specified peers at startup. This creates a persistent +; connection to a target peer. This is recommended as there aren't many +; neutrino compliant full nodes on the test network yet. +;neutrino.connect= + +; Add a peer to connect with at startup. +;neutrino.addpeer= + [Litecoin] @@ -136,27 +184,6 @@ bitcoin.simnet=1 ; active. ; litecoin.active=1 -; The host that your local ltcd daemon is listening on. This MUST be set if -; neutrino mode isn't active. -; litecoin.rpchost=localhost - -; Username for RPC connections to ltcd. This only needs to be set if neutrino -; mode isn't active. -; litecoin.rpcuser= - -; Password for RPC connections to ltcd. This only needs to be set if neutrino -; mode isn't active. -; litecoin.rpcpass= - -; File containing the daemon's certificate file. This only needs to be set if -; the node isn't on the same host as lnd. -; litecoin.rpccert=~/.btcd/rpc.cert - -; The raw bytes of the daemon's PEM-encoded certificate chain which will be used -; to authenticate the RPC connection. This only needs to be set if the ltcd -; node is on a remote host. -; litecoin.rawrpccert= - ; Use Bitcoin's test network. ; litecoin.testnet=1 ; @@ -166,21 +193,37 @@ litecoin.simnet=1 ; Use Bitcoin's regression test network ; litecoin.regtest=false +; Use the ltcd back-end (marked as btcd). No other back-end type is currently +; supported for litecoin. +litecoin.node=btcd -[neutrino] -; If the light client mode should be active or not. This mode requires less -; disk space as it doesn't require one to have full-node locally. Instead, -; neutrino will connect to the P2P network for all of lnd's needs. -neutrino.active=false +[Ltcd] -; Connect only to the specified peers at startup. This creates a persistent -; connection to a target peer. This is recommend as there aren't many neutrino -; compliant full nodes on the test network yet. -;neutrino.connect= +; The host that your local ltcd daemon is listening on. By default, this +; setting is assumed to be localhost with the default port for the current +; network. +; ltcd.rpchost=localhost + +; Username for RPC connections to ltcd. By default, lnd will attempt to +; automatically obtain the credentials, so this likely won't need to be set +; (other than for simnet mode). +; ltcd.rpcuser=kek + +; Password for RPC connections to ltcd. By default, lnd will attempt to +; automatically obtain the credentials, so this likely won't need to be set +; (other than for simnet mode). +; ltcd.rpcpass=kek + +; File containing the daemon's certificate file. This only needs to be set if +; the node isn't on the same host as lnd. +; ltcd.rpccert=~/.ltcd/rpc.cert + +; The raw bytes of the daemon's PEM-encoded certificate chain which will be used +; to authenticate the RPC connection. This only needs to be set if the ltcd +; node is on a remote host. +; ltcd.rawrpccert= -; Add a peer to connect with at startup. -;neutrino.addpeer= [autopilot] From ebd97b83f39733d2c8dd8c2407f612a16055a3ca Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 16 Jan 2018 17:18:15 -0800 Subject: [PATCH 35/39] discovery: ensure we only send node announcements that have active channels --- discovery/gossiper.go | 75 +++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/discovery/gossiper.go b/discovery/gossiper.go index 4dedfae2..a23af244 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -224,16 +224,35 @@ func (d *AuthenticatedGossiper) SynchronizeNode(pub *btcec.PublicKey) error { // containing all the messages to be sent to the target peer. var announceMessages []lnwire.Message + makeNodeAnn := func(n *channeldb.LightningNode) *lnwire.NodeAnnouncement { + alias, _ := lnwire.NewNodeAlias(n.Alias) + return &lnwire.NodeAnnouncement{ + Signature: n.AuthSig, + Timestamp: uint32(n.LastUpdate.Unix()), + Addresses: n.Addresses, + NodeID: n.PubKey, + Features: n.Features.RawFeatureVector, + RGBColor: n.Color, + Alias: alias, + } + } + // As peers are expecting channel announcements before node // announcements, we first retrieve the initial announcement, as well as // the latest channel update announcement for both of the directed edges // that make up each channel, and queue these to be sent to the peer. - var numEdges uint32 + var ( + numEdges uint32 + numNodes uint32 + ) if err := d.cfg.Router.ForEachChannel(func(chanInfo *channeldb.ChannelEdgeInfo, e1, e2 *channeldb.ChannelEdgePolicy) error { + // First, using the parameters of the channel, along with the // channel authentication proof, we'll create re-create the - // original authenticated channel announcement. + // original authenticated channel announcement. If the channel + // also has known validated nodes, then we'll send that as + // well. if chanInfo.AuthProof != nil { chanAnn, e1Ann, e2Ann := createChanAnnouncement( chanInfo.AuthProof, chanInfo, e1, e2) @@ -241,9 +260,29 @@ func (d *AuthenticatedGossiper) SynchronizeNode(pub *btcec.PublicKey) error { announceMessages = append(announceMessages, chanAnn) if e1Ann != nil { announceMessages = append(announceMessages, e1Ann) + + // If this edge has a validated node + // announcement, then we'll send that as well. + if e1.Node.HaveNodeAnnouncement { + nodeAnn := makeNodeAnn(e1.Node) + announceMessages = append( + announceMessages, nodeAnn, + ) + numNodes++ + } } if e2Ann != nil { announceMessages = append(announceMessages, e2Ann) + + // If this edge has a validated node + // announcement, then we'll send that as well. + if e2.Node.HaveNodeAnnouncement { + nodeAnn := makeNodeAnn(e2.Node) + announceMessages = append( + announceMessages, nodeAnn, + ) + numNodes++ + } } numEdges++ @@ -255,38 +294,6 @@ func (d *AuthenticatedGossiper) SynchronizeNode(pub *btcec.PublicKey) error { return err } - // Run through all the vertexes in the graph, retrieving the data for - // the node announcements we originally retrieved. - var numNodes uint32 - if err := d.cfg.Router.ForEachNode(func(node *channeldb.LightningNode) error { - // If this is a node we never received a node announcement for, - // we skip it. - if !node.HaveNodeAnnouncement { - return nil - } - - alias, err := lnwire.NewNodeAlias(node.Alias) - if err != nil { - return err - } - ann := &lnwire.NodeAnnouncement{ - Signature: node.AuthSig, - Timestamp: uint32(node.LastUpdate.Unix()), - Addresses: node.Addresses, - NodeID: node.PubKey, - Alias: alias, - Features: node.Features.RawFeatureVector, - RGBColor: node.Color, - } - announceMessages = append(announceMessages, ann) - - numNodes++ - - return nil - }); err != nil { - return err - } - log.Infof("Syncing channel graph state with %x, sending %v "+ "vertexes and %v edges", pub.SerializeCompressed(), numNodes, numEdges) From 7b6f4d34b1d6c56aaa0164d031d8fece9ba906c2 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 16 Jan 2018 17:19:33 -0800 Subject: [PATCH 36/39] docker: fix stray quote in start-lnd.sh Fixes #613. --- docker/lnd/start-lnd.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/lnd/start-lnd.sh b/docker/lnd/start-lnd.sh index e3367d9a..87928dcc 100755 --- a/docker/lnd/start-lnd.sh +++ b/docker/lnd/start-lnd.sh @@ -45,8 +45,8 @@ DEBUG=$(set_default "$DEBUG" "debug") NETWORK=$(set_default "$NETWORK" "simnet") CHAIN=$(set_default "$CHAIN" "bitcoin") BACKEND="btcd" -if [[ "$CHAIN" == "litecoin" ]] - BACKEND="ltcd +if [[ "$CHAIN" == "litecoin" ]]; then + BACKEND="ltcd" fi lnd \ From d512642c02b729f3aac4dd9aa022b190287cb289 Mon Sep 17 00:00:00 2001 From: romanornr Date: Wed, 17 Jan 2018 17:07:31 +0100 Subject: [PATCH 37/39] Travis: fix sudo required and apt-get update command --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 46c1a456..6594bb60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,11 @@ language: go go: - 1.8.5 - 1.9.2 -sudo: false +sudo: required install: - - add-apt-repository -y ppa:bitcoin/bitcoin - - apt-update - - apt-get -y install bitcoind + - sudo add-apt-repository -y ppa:bitcoin/bitcoin -y + - sudo apt-get update -q + - sudo apt-get install bitcoind -y - GLIDE_TAG=v0.12.3 - GLIDE_DOWNLOAD="https://github.com/Masterminds/glide/releases/download/$GLIDE_TAG/glide-$GLIDE_TAG-linux-amd64.tar.gz" - curl -L $GLIDE_DOWNLOAD | tar -xvz From a8728ccb5ba2905ca7308a5c7a6f45e49951c76c Mon Sep 17 00:00:00 2001 From: Maarten Tutak Date: Wed, 13 Dec 2017 18:46:26 +0100 Subject: [PATCH 38/39] docs: add example of Java gRPC client --- docs/grpc/java.md | 184 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 docs/grpc/java.md diff --git a/docs/grpc/java.md b/docs/grpc/java.md new file mode 100644 index 00000000..23a0878e --- /dev/null +++ b/docs/grpc/java.md @@ -0,0 +1,184 @@ + +# How to write a Java gRPC client for the Lightning Network Daemon + +This section enumerates what you need to do to write a client that communicates +with lnd in Java. We'll be using Maven as our build tool. + +### Prerequisites + - Maven + - running lnd + - running btcd + +### Setup and Installation +#### Project Structure +``` +. +├── pom.xml +└── src + ├── main + ├── java + │ └── Main.java + ├── proto + ├── google + │ └── api + │ ├── annotations.proto + │ └── http.proto + └── lnrpc + └── rpc.proto + +``` +Note the ***proto*** folder, where all the proto files are kept. + + - [rpc.proto](https://github.com/lightningnetwork/lnd/blob/master/lnrpc/rpc.proto) + - [annotations.proto](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/third_party/googleapis/google/api/annotations.proto) + - [http.proto](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/third_party/googleapis/google/api/http.proto) + +#### pom.xml +``` + + 1.8.0 + +``` +The following dependencies are required. +``` + + + io.grpc + grpc-netty + ${grpc.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + io.netty + netty-tcnative-boringssl-static + 2.0.7.Final + + +``` +In the build section, we'll need to configure the following things : +``` + + + + kr.motd.maven + os-maven-plugin + 1.5.0.Final + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.5.0 + + com.google.protobuf:protoc:3.4.0:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + + compile + compile-custom + + + + + + +``` +#### Main.java +```java +import io.grpc.ManagedChannel; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyChannelBuilder; +import io.netty.handler.ssl.SslContext; +import lnrpc.LightningGrpc; +import lnrpc.LightningGrpc.LightningBlockingStub; +import lnrpc.Rpc.*; + +import javax.net.ssl.SSLException; +import java.io.File; + +public class Main { + + private static final String CERT_PATH = "/Users/user/Library/Application Support/Lnd/tls.cert"; + private static final String HOST = "localhost"; + private static final int PORT = 10009; + + public static void main(String... args) throws SSLException { + SslContext sslContext = GrpcSslContexts.forClient().trustManager(new File(CERT_PATH)).build(); + NettyChannelBuilder channelBuilder = NettyChannelBuilder.forAddress(HOST, PORT); + ManagedChannel channel = channelBuilder.sslContext(sslContext).build(); + LightningBlockingStub stub = LightningGrpc.newBlockingStub(channel); + + GetInfoResponse response = stub.getInfo(GetInfoRequest.getDefaultInstance()); + System.out.println(response.getIdentityPubkey()); + } +} +``` +#### Running the example +Execute the following command in the directory where the **pom.xml** file is located. +``` +mvn compile exec:java -Dexec.mainClass="Main" -Dexec.cleanupDaemonThreads=false +``` +##### Sample ouput +``` +[INFO] Scanning for projects... +[INFO] ------------------------------------------------------------------------ +[INFO] Detecting the operating system and CPU architecture +[INFO] ------------------------------------------------------------------------ +[INFO] os.detected.name: osx +[INFO] os.detected.arch: x86_64 +[INFO] os.detected.version: 10.13 +[INFO] os.detected.version.major: 10 +[INFO] os.detected.version.minor: 13 +[INFO] os.detected.classifier: osx-x86_64 +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] Building lightning-client 0.0.1-SNAPSHOT +[INFO] ------------------------------------------------------------------------ +[INFO] +[INFO] --- protobuf-maven-plugin:0.5.0:compile (default) @ lightning-client --- +[INFO] Compiling 3 proto file(s) to /Users/user/Documents/Projects/lightningclient/target/generated-sources/protobuf/java +[INFO] +[INFO] --- protobuf-maven-plugin:0.5.0:compile-custom (default) @ lightning-client --- +[INFO] Compiling 3 proto file(s) to /Users/user/Documents/Projects/lightningclient/target/generated-sources/protobuf/grpc-java +[INFO] +[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ lightning-client --- +[INFO] Using 'UTF-8' encoding to copy filtered resources. +[INFO] Copying 0 resource +[INFO] Copying 3 resources +[INFO] Copying 3 resources +[INFO] +[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ lightning-client --- +[INFO] Changes detected - recompiling the module! +[INFO] Compiling 12 source files to /Users/user/Documents/Projects/lightningclient/target/classes +[INFO] +[INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ lightning-client --- +032562215c38dede6f1f2f262ff4c8db58a38ecf889e8e907eee8e4c320e0b5e81 +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 7.408 s +[INFO] Finished at: 2018-01-13T19:05:49+01:00 +[INFO] Final Memory: 30M/589M +[INFO] ------------------------------------------------------------------------ +``` + +### Java proto options + +There are 2 options available that can be used in the *rpc.proto* file : + +* option java_multiple_files = true; +* option java_package = "network.lightning.rpc"; +>The package you want to use for your generated Java classes. If no explicit java_package option is given in the .proto file, then by default the proto package (specified using the "package" keyword in the .proto file) will be used. However, proto packages generally do not make good Java packages since proto packages are not expected to start with reverse domain names. If not generating Java code, this option has no effect. \ No newline at end of file From e4e6766746c5070c4749b08cd40672a0348b61bc Mon Sep 17 00:00:00 2001 From: Deelight-fr Date: Thu, 18 Jan 2018 23:51:35 +0100 Subject: [PATCH 39/39] Doc fix: typo in lnd/bitcoind arguments --- docs/INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/INSTALL.md b/docs/INSTALL.md index b58da946..d720d047 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -189,7 +189,7 @@ command after `bitcoind` has finished syncing on testnet. Otherwise, replace and `rpcpass` parameters can typically be determined by `lnd` for a `bitcoind` instance running under the same user, including when using cookie auth. ``` -lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug --bitcoin.node=bitcoind --bitcoind.rpcuser=kek bitcoind.rpcpass=kek --externalip=X.X.X.X +lnd --bitcoin.active --bitcoin.testnet --debuglevel=debug --bitcoin.node=bitcoind --bitcoind.rpcuser=kek --bitcoind.rpcpass=kek --externalip=X.X.X.X ``` #### Network Reachability