From 8e46b9ea5d294e44035a47fe763c9aba4d07d42e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 4 Jan 2022 20:33:49 +0100 Subject: [PATCH 01/24] paych: API to pre-fund channels --- api/api_full.go | 6 +- api/proxy_gen.go | 8 +-- api/v0api/v1_wrapper.go | 5 ++ markets/retrievaladapter/client.go | 2 +- node/impl/paych/paych.go | 6 +- paychmgr/cbor_gen.go | 54 ++++++++++++++++- paychmgr/manager.go | 4 +- paychmgr/simple.go | 97 +++++++++++++++++++++++++----- paychmgr/store.go | 19 +++--- 9 files changed, 168 insertions(+), 33 deletions(-) diff --git a/api/api_full.go b/api/api_full.go index cf58a3cc6ee..cf9a1f22ef2 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -689,7 +689,11 @@ type FullNode interface { // MethodGroup: Paych // The Paych methods are for interacting with and managing payment channels - PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt) (*ChannelInfo, error) //perm:sign + // PaychGet gets or creates a payment channel between address pair + // - If reserve is false, the specified amount will be added to the channel through on-chain send for future use + // - If reserve is true, the specified amount will be reserved for use. If there aren't enough non-reserved funds + // available, funds will be added through an on-chain message. + PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt, reserve bool) (*ChannelInfo, error) //perm:sign PaychGetWaitReady(context.Context, cid.Cid) (address.Address, error) //perm:sign PaychAvailableFunds(ctx context.Context, ch address.Address) (*ChannelAvailableFunds, error) //perm:sign PaychAvailableFundsByFromTo(ctx context.Context, from, to address.Address) (*ChannelAvailableFunds, error) //perm:sign diff --git a/api/proxy_gen.go b/api/proxy_gen.go index e353a7c6e89..9a134cec91b 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -306,7 +306,7 @@ type FullNodeStruct struct { PaychCollect func(p0 context.Context, p1 address.Address) (cid.Cid, error) `perm:"sign"` - PaychGet func(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt) (*ChannelInfo, error) `perm:"sign"` + PaychGet func(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt, p4 bool) (*ChannelInfo, error) `perm:"sign"` PaychGetWaitReady func(p0 context.Context, p1 cid.Cid) (address.Address, error) `perm:"sign"` @@ -2179,14 +2179,14 @@ func (s *FullNodeStub) PaychCollect(p0 context.Context, p1 address.Address) (cid return *new(cid.Cid), ErrNotSupported } -func (s *FullNodeStruct) PaychGet(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt) (*ChannelInfo, error) { +func (s *FullNodeStruct) PaychGet(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt, p4 bool) (*ChannelInfo, error) { if s.Internal.PaychGet == nil { return nil, ErrNotSupported } - return s.Internal.PaychGet(p0, p1, p2, p3) + return s.Internal.PaychGet(p0, p1, p2, p3, p4) } -func (s *FullNodeStub) PaychGet(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt) (*ChannelInfo, error) { +func (s *FullNodeStub) PaychGet(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt, p4 bool) (*ChannelInfo, error) { return nil, ErrNotSupported } diff --git a/api/v0api/v1_wrapper.go b/api/v0api/v1_wrapper.go index 7e0d7a94ab6..605b27b0c9f 100644 --- a/api/v0api/v1_wrapper.go +++ b/api/v0api/v1_wrapper.go @@ -337,4 +337,9 @@ func (w *WrapperV1Full) clientRetrieve(ctx context.Context, order RetrievalOrder finish(w.ClientExport(ctx, eref, *ref)) } +func (w *WrapperV1Full) PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt) (*api.ChannelInfo, error) { + // v0 always reserves + return w.FullNode.PaychGet(ctx, from, to, amt, true) +} + var _ FullNode = &WrapperV1Full{} diff --git a/markets/retrievaladapter/client.go b/markets/retrievaladapter/client.go index 1bef23e1296..4ed2e905ab4 100644 --- a/markets/retrievaladapter/client.go +++ b/markets/retrievaladapter/client.go @@ -34,7 +34,7 @@ func NewRetrievalClientNode(payAPI payapi.PaychAPI, chainAPI full.ChainAPI, stat func (rcn *retrievalClientNode) GetOrCreatePaymentChannel(ctx context.Context, clientAddress address.Address, minerAddress address.Address, clientFundsAvailable abi.TokenAmount, tok shared.TipSetToken) (address.Address, cid.Cid, error) { // TODO: respect the provided TipSetToken (a serialized TipSetKey) when // querying the chain - ci, err := rcn.payAPI.PaychGet(ctx, clientAddress, minerAddress, clientFundsAvailable) + ci, err := rcn.payAPI.PaychGet(ctx, clientAddress, minerAddress, clientFundsAvailable, true) if err != nil { return address.Undef, cid.Undef, err } diff --git a/node/impl/paych/paych.go b/node/impl/paych/paych.go index df3b1e3e490..d308f6248ac 100644 --- a/node/impl/paych/paych.go +++ b/node/impl/paych/paych.go @@ -22,8 +22,8 @@ type PaychAPI struct { PaychMgr *paychmgr.Manager } -func (a *PaychAPI) PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt) (*api.ChannelInfo, error) { - ch, mcid, err := a.PaychMgr.GetPaych(ctx, from, to, amt) +func (a *PaychAPI) PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt, reserve bool) (*api.ChannelInfo, error) { + ch, mcid, err := a.PaychMgr.GetPaych(ctx, from, to, amt, reserve) if err != nil { return nil, err } @@ -55,7 +55,7 @@ func (a *PaychAPI) PaychNewPayment(ctx context.Context, from, to address.Address // TODO: Fix free fund tracking in PaychGet // TODO: validate voucher spec before locking funds - ch, err := a.PaychGet(ctx, from, to, amount) + ch, err := a.PaychGet(ctx, from, to, amount, true) if err != nil { return nil, err } diff --git a/paychmgr/cbor_gen.go b/paychmgr/cbor_gen.go index caa4143a257..428c09a9e05 100644 --- a/paychmgr/cbor_gen.go +++ b/paychmgr/cbor_gen.go @@ -196,7 +196,7 @@ func (t *ChannelInfo) MarshalCBOR(w io.Writer) error { _, err := w.Write(cbg.CborNull) return err } - if _, err := w.Write([]byte{172}); err != nil { + if _, err := w.Write([]byte{174}); err != nil { return err } @@ -346,6 +346,38 @@ func (t *ChannelInfo) MarshalCBOR(w io.Writer) error { return err } + // t.AvailableAmount (big.Int) (struct) + if len("AvailableAmount") > cbg.MaxLength { + return xerrors.Errorf("Value in field \"AvailableAmount\" was too long") + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajTextString, uint64(len("AvailableAmount"))); err != nil { + return err + } + if _, err := io.WriteString(w, string("AvailableAmount")); err != nil { + return err + } + + if err := t.AvailableAmount.MarshalCBOR(w); err != nil { + return err + } + + // t.PendingAvailableAmount (big.Int) (struct) + if len("PendingAvailableAmount") > cbg.MaxLength { + return xerrors.Errorf("Value in field \"PendingAvailableAmount\" was too long") + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajTextString, uint64(len("PendingAvailableAmount"))); err != nil { + return err + } + if _, err := io.WriteString(w, string("PendingAvailableAmount")); err != nil { + return err + } + + if err := t.PendingAvailableAmount.MarshalCBOR(w); err != nil { + return err + } + // t.PendingAmount (big.Int) (struct) if len("PendingAmount") > cbg.MaxLength { return xerrors.Errorf("Value in field \"PendingAmount\" was too long") @@ -577,6 +609,26 @@ func (t *ChannelInfo) UnmarshalCBOR(r io.Reader) error { return xerrors.Errorf("unmarshaling t.Amount: %w", err) } + } + // t.AvailableAmount (big.Int) (struct) + case "AvailableAmount": + + { + + if err := t.AvailableAmount.UnmarshalCBOR(br); err != nil { + return xerrors.Errorf("unmarshaling t.AvailableAmount: %w", err) + } + + } + // t.PendingAvailableAmount (big.Int) (struct) + case "PendingAvailableAmount": + + { + + if err := t.PendingAvailableAmount.UnmarshalCBOR(br); err != nil { + return xerrors.Errorf("unmarshaling t.PendingAvailableAmount: %w", err) + } + } // t.PendingAmount (big.Int) (struct) case "PendingAmount": diff --git a/paychmgr/manager.go b/paychmgr/manager.go index e0fcd7a754c..eed475547aa 100644 --- a/paychmgr/manager.go +++ b/paychmgr/manager.go @@ -101,13 +101,13 @@ func (pm *Manager) Stop() error { return nil } -func (pm *Manager) GetPaych(ctx context.Context, from, to address.Address, amt types.BigInt) (address.Address, cid.Cid, error) { +func (pm *Manager) GetPaych(ctx context.Context, from, to address.Address, amt types.BigInt, reserve bool) (address.Address, cid.Cid, error) { chanAccessor, err := pm.accessorByFromTo(from, to) if err != nil { return address.Undef, cid.Undef, err } - return chanAccessor.getPaych(ctx, amt) + return chanAccessor.getPaych(ctx, amt, reserve) } func (pm *Manager) AvailableFunds(ctx context.Context, ch address.Address) (*api.ChannelAvailableFunds, error) { diff --git a/paychmgr/simple.go b/paychmgr/simple.go index 502338e2953..fd849d3aed7 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "sort" "sync" "github.com/ipfs/go-cid" @@ -32,18 +33,20 @@ type fundsReq struct { ctx context.Context promise chan *paychFundsRes amt types.BigInt + reserve bool lk sync.Mutex // merge parent, if this req is part of a merge merge *mergedFundsReq } -func newFundsReq(ctx context.Context, amt types.BigInt) *fundsReq { +func newFundsReq(ctx context.Context, amt types.BigInt, reserve bool) *fundsReq { promise := make(chan *paychFundsRes) return &fundsReq{ ctx: ctx, promise: promise, amt: amt, + reserve: reserve, } } @@ -104,6 +107,15 @@ func newMergedFundsReq(reqs []*fundsReq) *mergedFundsReq { r.setMergeParent(m) } + sort.Slice(m.reqs, func(i, j int) bool { + if m.reqs[i].reserve != m.reqs[j].reserve { // non-reserve first + return m.reqs[j].reserve + } + + // sort by amount asc (reducing latency for smaller requests) + return m.reqs[i].amt.LessThan(m.reqs[j].amt) + }) + // If the requests were all cancelled while being added, cancel the context // immediately m.checkActive() @@ -135,18 +147,24 @@ func (m *mergedFundsReq) onComplete(res *paychFundsRes) { } // sum is the sum of the amounts in all requests in the merge -func (m *mergedFundsReq) sum() types.BigInt { +func (m *mergedFundsReq) sum() (types.BigInt, types.BigInt) { sum := types.NewInt(0) + avail := types.NewInt(0) + for _, r := range m.reqs { if r.isActive() { sum = types.BigAdd(sum, r.amt) + if !r.reserve { + avail = types.BigAdd(avail, r.amt) + } } } - return sum + + return sum, avail } // getPaych ensures that a channel exists between the from and to addresses, -// and adds the given amount of funds. +// and reserves (or adds as available) the given amount of funds. // If the channel does not exist a create channel message is sent and the // message CID is returned. // If the channel does exist an add funds message is sent and both the channel @@ -156,9 +174,9 @@ func (m *mergedFundsReq) sum() types.BigInt { // address and the CID of the new add funds message. // If an operation returns an error, subsequent waiting operations will still // be attempted. -func (ca *channelAccessor) getPaych(ctx context.Context, amt types.BigInt) (address.Address, cid.Cid, error) { +func (ca *channelAccessor) getPaych(ctx context.Context, amt types.BigInt, reserve bool) (address.Address, cid.Cid, error) { // Add the request to add funds to a queue and wait for the result - freq := newFundsReq(ctx, amt) + freq := newFundsReq(ctx, amt, reserve) ca.enqueue(ctx, freq) select { case res := <-freq.promise: @@ -195,14 +213,57 @@ func (ca *channelAccessor) processQueue(ctx context.Context, channelID string) ( // For example if there are pending requests for 3, 2, 4 then // amt = 3 + 2 + 4 = 9 merged := newMergedFundsReq(ca.fundsReqQueue) - amt := merged.sum() + amt, avail := merged.sum() if amt.IsZero() { // Note: The amount can be zero if requests are cancelled as we're // building the mergedFundsReq return ca.currentAvailableFunds(ctx, channelID, amt) } - res := ca.processTask(merged.ctx, amt) + { + toReserve := types.BigSub(amt, avail) + avail := types.NewInt(0) + + // reserve at most what we need + ca.mutateChannelInfo(ctx, channelID, func(ci *ChannelInfo) { + avail = ci.AvailableAmount + if avail.GreaterThan(toReserve) { + avail = toReserve + } + ci.AvailableAmount = big.Sub(ci.AvailableAmount, avail) + }) + + used := types.NewInt(0) + + next := 0 + for i, r := range merged.reqs { + if !r.reserve { + // non-reserving request are put after reserving requests, so we are done here + break + } + + if r.amt.GreaterThan(types.BigSub(avail, used)) { + // requests are sorted by amount ascending, so if we hit this, there aren't any more requests we can fill + } + + // don't try to fill inactive requests + if !r.isActive() { + continue + } + + used = types.BigAdd(used, r.amt) + r.onComplete(&paychFundsRes{}) + next = i + 1 + } + merged.reqs = merged.reqs[next:] + + // return any unused reserved funds (e.g. from cancelled requests) + ca.mutateChannelInfo(ctx, channelID, func(ci *ChannelInfo) { + ci.AvailableAmount = types.BigAdd(ci.AvailableAmount, types.BigSub(avail, used)) + }) + } + + res := ca.processTask(merged.ctx, amt, avail) // If the task is waiting on an external event (eg something to appear on // chain) it will return nil @@ -333,7 +394,7 @@ func (ca *channelAccessor) currentAvailableFunds(ctx context.Context, channelID // Note that processTask may be called repeatedly in the same state, and should // return nil if there is no state change to be made (eg when waiting for a // message to be confirmed on chain) -func (ca *channelAccessor) processTask(ctx context.Context, amt types.BigInt) *paychFundsRes { +func (ca *channelAccessor) processTask(ctx context.Context, amt, avail types.BigInt) *paychFundsRes { // Get the payment channel for the from/to addresses. // Note: It's ok if we get ErrChannelNotTracked. It just means we need to // create a channel. @@ -344,7 +405,7 @@ func (ca *channelAccessor) processTask(ctx context.Context, amt types.BigInt) *p // If a channel has not yet been created, create one. if channelInfo == nil { - mcid, err := ca.createPaych(ctx, amt) + mcid, err := ca.createPaych(ctx, amt, avail) if err != nil { return &paychFundsRes{err: err} } @@ -368,7 +429,7 @@ func (ca *channelAccessor) processTask(ctx context.Context, amt types.BigInt) *p // We need to add more funds, so send an add funds message to // cover the amount for this request - mcid, err := ca.addFunds(ctx, channelInfo, amt) + mcid, err := ca.addFunds(ctx, channelInfo, amt, avail) if err != nil { return &paychFundsRes{err: err} } @@ -376,7 +437,7 @@ func (ca *channelAccessor) processTask(ctx context.Context, amt types.BigInt) *p } // createPaych sends a message to create the channel and returns the message cid -func (ca *channelAccessor) createPaych(ctx context.Context, amt types.BigInt) (cid.Cid, error) { +func (ca *channelAccessor) createPaych(ctx context.Context, amt, avail types.BigInt) (cid.Cid, error) { mb, err := ca.messageBuilder(ctx, ca.from) if err != nil { return cid.Undef, err @@ -393,7 +454,7 @@ func (ca *channelAccessor) createPaych(ctx context.Context, amt types.BigInt) (c mcid := smsg.Cid() // Create a new channel in the store - ci, err := ca.store.CreateChannel(ctx, ca.from, ca.to, mcid, amt) + ci, err := ca.store.CreateChannel(ctx, ca.from, ca.to, mcid, amt, avail) if err != nil { log.Errorf("creating channel: %s", err) return cid.Undef, err @@ -452,7 +513,9 @@ func (ca *channelAccessor) waitPaychCreateMsg(ctx context.Context, channelID str ca.mutateChannelInfo(ctx, channelID, func(channelInfo *ChannelInfo) { channelInfo.Channel = &decodedReturn.RobustAddress channelInfo.Amount = channelInfo.PendingAmount + channelInfo.AvailableAmount = channelInfo.PendingAvailableAmount channelInfo.PendingAmount = big.NewInt(0) + channelInfo.PendingAvailableAmount = big.NewInt(0) channelInfo.CreateMsg = nil }) @@ -460,7 +523,7 @@ func (ca *channelAccessor) waitPaychCreateMsg(ctx context.Context, channelID str } // addFunds sends a message to add funds to the channel and returns the message cid -func (ca *channelAccessor) addFunds(ctx context.Context, channelInfo *ChannelInfo, amt types.BigInt) (*cid.Cid, error) { +func (ca *channelAccessor) addFunds(ctx context.Context, channelInfo *ChannelInfo, amt, avail types.BigInt) (*cid.Cid, error) { msg := &types.Message{ To: *channelInfo.Channel, From: channelInfo.Control, @@ -477,6 +540,7 @@ func (ca *channelAccessor) addFunds(ctx context.Context, channelInfo *ChannelInf // Store the add funds message CID on the channel ca.mutateChannelInfo(ctx, channelInfo.ChannelID, func(ci *ChannelInfo) { ci.PendingAmount = amt + ci.PendingAvailableAmount = avail ci.AddFundsMsg = &mcid }) @@ -492,6 +556,8 @@ func (ca *channelAccessor) addFunds(ctx context.Context, channelInfo *ChannelInf return &mcid, nil } +// TODO func (ca *channelAccessor) freeFunds(ctx context.Context, channelInfo *ChannelInfo, amt, avail types.BigInt) (*cid.Cid, error) { + // waitForAddFundsMsg waits for mcid to appear on chain and returns error, if any func (ca *channelAccessor) waitForAddFundsMsg(ctx context.Context, channelID string, mcid cid.Cid) { err := ca.waitAddFundsMsg(ctx, channelID, mcid) @@ -514,6 +580,7 @@ func (ca *channelAccessor) waitAddFundsMsg(ctx context.Context, channelID string ca.mutateChannelInfo(ctx, channelID, func(channelInfo *ChannelInfo) { channelInfo.PendingAmount = big.NewInt(0) + channelInfo.PendingAvailableAmount = big.NewInt(0) channelInfo.AddFundsMsg = nil }) @@ -526,7 +593,9 @@ func (ca *channelAccessor) waitAddFundsMsg(ctx context.Context, channelID string // Store updated amount ca.mutateChannelInfo(ctx, channelID, func(channelInfo *ChannelInfo) { channelInfo.Amount = types.BigAdd(channelInfo.Amount, channelInfo.PendingAmount) + channelInfo.AvailableAmount = types.BigAdd(channelInfo.AvailableAmount, channelInfo.PendingAvailableAmount) channelInfo.PendingAmount = big.NewInt(0) + channelInfo.PendingAvailableAmount = big.NewInt(0) channelInfo.AddFundsMsg = nil }) diff --git a/paychmgr/store.go b/paychmgr/store.go index 62849e6bedd..bbc549b862c 100644 --- a/paychmgr/store.go +++ b/paychmgr/store.go @@ -74,6 +74,10 @@ type ChannelInfo struct { // has locally been added to the channel. It should reflect the channel's // Balance on chain as long as all operations occur on the same datastore. Amount types.BigInt + // AvailableAmount indicates how much afil is non-reverved + AvailableAmount types.BigInt + // PendingAvailableAmount is available amount that we're awaiting confirmation of + PendingAvailableAmount types.BigInt // PendingAmount is the amount that we're awaiting confirmation of PendingAmount types.BigInt // CreateMsg is the CID of a pending create message (while waiting for confirmation) @@ -416,14 +420,15 @@ func (ps *Store) ByChannelID(ctx context.Context, channelID string) (*ChannelInf } // CreateChannel creates an outbound channel for the given from / to -func (ps *Store) CreateChannel(ctx context.Context, from address.Address, to address.Address, createMsgCid cid.Cid, amt types.BigInt) (*ChannelInfo, error) { +func (ps *Store) CreateChannel(ctx context.Context, from address.Address, to address.Address, createMsgCid cid.Cid, amt, avail types.BigInt) (*ChannelInfo, error) { ci := &ChannelInfo{ - Direction: DirOutbound, - NextLane: 0, - Control: from, - Target: to, - CreateMsg: &createMsgCid, - PendingAmount: amt, + Direction: DirOutbound, + NextLane: 0, + Control: from, + Target: to, + CreateMsg: &createMsgCid, + PendingAmount: amt, + PendingAvailableAmount: avail, } // Save the new channel From 97151138984b312d64d2679fa8d6d536ff84abaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 4 Jan 2022 21:34:27 +0100 Subject: [PATCH 02/24] paych: Test pre-funding --- api/mocks/mock_full.go | 8 +- documentation/en/api-v1-unstable-methods.md | 9 +- paychmgr/paychget_test.go | 319 ++++++++++++++++++-- paychmgr/paychvoucherfunds_test.go | 4 +- paychmgr/settle_test.go | 4 +- paychmgr/simple.go | 101 ++++--- 6 files changed, 360 insertions(+), 85 deletions(-) diff --git a/api/mocks/mock_full.go b/api/mocks/mock_full.go index e985a794d0c..c745f26fcd4 100644 --- a/api/mocks/mock_full.go +++ b/api/mocks/mock_full.go @@ -1976,18 +1976,18 @@ func (mr *MockFullNodeMockRecorder) PaychCollect(arg0, arg1 interface{}) *gomock } // PaychGet mocks base method. -func (m *MockFullNode) PaychGet(arg0 context.Context, arg1, arg2 address.Address, arg3 big.Int) (*api.ChannelInfo, error) { +func (m *MockFullNode) PaychGet(arg0 context.Context, arg1, arg2 address.Address, arg3 big.Int, arg4 bool) (*api.ChannelInfo, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PaychGet", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "PaychGet", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(*api.ChannelInfo) ret1, _ := ret[1].(error) return ret0, ret1 } // PaychGet indicates an expected call of PaychGet. -func (mr *MockFullNodeMockRecorder) PaychGet(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +func (mr *MockFullNodeMockRecorder) PaychGet(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaychGet", reflect.TypeOf((*MockFullNode)(nil).PaychGet), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaychGet", reflect.TypeOf((*MockFullNode)(nil).PaychGet), arg0, arg1, arg2, arg3, arg4) } // PaychGetWaitReady mocks base method. diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index a5fdd999429..ce0ceadcdfd 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -4509,7 +4509,11 @@ Response: ``` ### PaychGet -There are not yet any comments for this method. +PaychGet gets or creates a payment channel between address pair + - If reserve is false, the specified amount will be added to the channel through on-chain send for future use + - If reserve is true, the specified amount will be reserved for use. If there aren't enough non-reserved funds + available, funds will be added through an on-chain message. + Perms: sign @@ -4518,7 +4522,8 @@ Inputs: [ "f01234", "f01234", - "0" + "0", + true ] ``` diff --git a/paychmgr/paychget_test.go b/paychmgr/paychget_test.go index 2aacaf6c287..31207917721 100644 --- a/paychmgr/paychget_test.go +++ b/paychmgr/paychget_test.go @@ -55,7 +55,7 @@ func TestPaychGetCreateChannelMsg(t *testing.T) { require.NoError(t, err) amt := big.NewInt(10) - ch, mcid, err := mgr.GetPaych(ctx, from, to, amt) + ch, mcid, err := mgr.GetPaych(ctx, from, to, amt, true) require.NoError(t, err) require.Equal(t, address.Undef, ch) @@ -83,7 +83,7 @@ func TestPaychGetCreateChannelThenAddFunds(t *testing.T) { // Send create message for a channel with value 10 amt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, true) require.NoError(t, err) // Should have no channels yet (message sent but channel not created) @@ -100,7 +100,7 @@ func TestPaychGetCreateChannelThenAddFunds(t *testing.T) { // 2. Request add funds - should block until create channel has completed amt2 := big.NewInt(5) - ch2, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, amt2) + ch2, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, amt2, true) // 4. This GetPaych should return after create channel from first // GetPaych completes @@ -154,6 +154,74 @@ func TestPaychGetCreateChannelThenAddFunds(t *testing.T) { <-done } +func TestPaychGetCreatePrefundedChannelThenAddFunds(t *testing.T) { + ctx := context.Background() + store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore())) + + ch := tutils.NewIDAddr(t, 100) + from := tutils.NewIDAddr(t, 101) + to := tutils.NewIDAddr(t, 102) + + mock := newMockManagerAPI() + defer mock.close() + + mgr, err := newManager(store, mock) + require.NoError(t, err) + + // Send create message for a channel with value 10 + amt := big.NewInt(10) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, false) + require.NoError(t, err) + + // Should have no channels yet (message sent but channel not created) + cis, err := mgr.ListChannels(ctx) + require.NoError(t, err) + require.Len(t, cis, 0) + + // 1. Set up create channel response (sent in response to WaitForMsg()) + response := testChannelResponse(t, ch) + + done := make(chan struct{}) + go func() { + defer close(done) + + // 2. Request add funds - shouldn't block + amt2 := big.NewInt(3) + ch2, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, amt2, true) + + // 4. This GetPaych should return after create channel from first + // GetPaych completes + require.NoError(t, err) + + // Expect the channel to be the same + require.Equal(t, ch, ch2) + require.Equal(t, cid.Undef, addFundsMsgCid) + + // Should have one channel, whose address is the channel that was created + cis, err := mgr.ListChannels(ctx) + require.NoError(t, err) + require.Len(t, cis, 1) + require.Equal(t, ch, cis[0]) + + // Amount should be amount sent to first GetPaych (to create + // channel). + // PendingAmount should be zero, AvailableAmount should be Amount minus what we requested + + ci, err := mgr.GetChannelInfo(ctx, ch) + require.NoError(t, err) + require.EqualValues(t, 10, ci.Amount.Int64()) + require.EqualValues(t, 0, ci.PendingAmount.Int64()) + require.EqualValues(t, 7, ci.AvailableAmount.Int64()) + require.Nil(t, ci.CreateMsg) + require.Nil(t, ci.AddFundsMsg) + }() + + // 3. Send create channel response + mock.receiveMsgResponse(createMsgCid, response) + + <-done +} + // TestPaychGetCreateChannelWithErrorThenCreateAgain tests that if an // operation is queued up behind a create channel operation, and the create // channel fails, then the waiting operation can succeed. @@ -172,7 +240,7 @@ func TestPaychGetCreateChannelWithErrorThenCreateAgain(t *testing.T) { // Send create message for a channel amt := big.NewInt(10) - _, mcid1, err := mgr.GetPaych(ctx, from, to, amt) + _, mcid1, err := mgr.GetPaych(ctx, from, to, amt, true) require.NoError(t, err) // 1. Set up create channel response (sent in response to WaitForMsg()) @@ -190,7 +258,7 @@ func TestPaychGetCreateChannelWithErrorThenCreateAgain(t *testing.T) { // Because first channel create fails, this request // should be for channel create again. amt2 := big.NewInt(5) - ch2, mcid2, err := mgr.GetPaych(ctx, from, to, amt2) + ch2, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, true) require.NoError(t, err) require.Equal(t, address.Undef, ch2) @@ -237,7 +305,7 @@ func TestPaychGetRecoverAfterError(t *testing.T) { // Send create message for a channel amt := big.NewInt(10) - _, mcid, err := mgr.GetPaych(ctx, from, to, amt) + _, mcid, err := mgr.GetPaych(ctx, from, to, amt, true) require.NoError(t, err) // Send error create channel response @@ -248,7 +316,7 @@ func TestPaychGetRecoverAfterError(t *testing.T) { // Send create message for a channel again amt2 := big.NewInt(7) - _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2) + _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, true) require.NoError(t, err) // Send success create channel response @@ -289,7 +357,7 @@ func TestPaychGetRecoverAfterAddFundsError(t *testing.T) { // Send create message for a channel amt := big.NewInt(10) - _, mcid1, err := mgr.GetPaych(ctx, from, to, amt) + _, mcid1, err := mgr.GetPaych(ctx, from, to, amt, true) require.NoError(t, err) // Send success create channel response @@ -298,7 +366,7 @@ func TestPaychGetRecoverAfterAddFundsError(t *testing.T) { // Send add funds message for channel amt2 := big.NewInt(5) - _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2) + _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, true) require.NoError(t, err) // Send error add funds response @@ -325,7 +393,7 @@ func TestPaychGetRecoverAfterAddFundsError(t *testing.T) { // Send add funds message for channel again amt3 := big.NewInt(2) - _, mcid3, err := mgr.GetPaych(ctx, from, to, amt3) + _, mcid3, err := mgr.GetPaych(ctx, from, to, amt3, true) require.NoError(t, err) // Send success add funds response @@ -370,7 +438,7 @@ func TestPaychGetRestartAfterCreateChannelMsg(t *testing.T) { // Send create message for a channel with value 10 amt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, true) require.NoError(t, err) // Simulate shutting down system @@ -397,7 +465,7 @@ func TestPaychGetRestartAfterCreateChannelMsg(t *testing.T) { // 2. Request add funds - should block until create channel has completed amt2 := big.NewInt(5) - ch2, addFundsMsgCid, err := mgr2.GetPaych(ctx, from, to, amt2) + ch2, addFundsMsgCid, err := mgr2.GetPaych(ctx, from, to, amt2, true) // 4. This GetPaych should return after create channel from first // GetPaych completes @@ -449,7 +517,7 @@ func TestPaychGetRestartAfterAddFundsMsg(t *testing.T) { // Send create message for a channel amt := big.NewInt(10) - _, mcid1, err := mgr.GetPaych(ctx, from, to, amt) + _, mcid1, err := mgr.GetPaych(ctx, from, to, amt, true) require.NoError(t, err) // Send success create channel response @@ -458,7 +526,7 @@ func TestPaychGetRestartAfterAddFundsMsg(t *testing.T) { // Send add funds message for channel amt2 := big.NewInt(5) - _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2) + _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, true) require.NoError(t, err) // Simulate shutting down system @@ -512,7 +580,7 @@ func TestPaychGetWait(t *testing.T) { // 1. Get amt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, true) require.NoError(t, err) expch := tutils.NewIDAddr(t, 100) @@ -535,7 +603,7 @@ func TestPaychGetWait(t *testing.T) { // Request add funds amt2 := big.NewInt(15) - _, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, amt2) + _, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, amt2, true) require.NoError(t, err) go func() { @@ -569,7 +637,7 @@ func TestPaychGetWaitErr(t *testing.T) { // 1. Create channel amt := big.NewInt(10) - _, mcid, err := mgr.GetPaych(ctx, from, to, amt) + _, mcid, err := mgr.GetPaych(ctx, from, to, amt, true) require.NoError(t, err) done := make(chan address.Address) @@ -615,7 +683,7 @@ func TestPaychGetWaitCtx(t *testing.T) { require.NoError(t, err) amt := big.NewInt(10) - _, mcid, err := mgr.GetPaych(ctx, from, to, amt) + _, mcid, err := mgr.GetPaych(ctx, from, to, amt, true) require.NoError(t, err) // When the context is cancelled, should unblock wait @@ -646,7 +714,7 @@ func TestPaychGetMergeAddFunds(t *testing.T) { // Send create message for a channel with value 10 createAmt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, true) require.NoError(t, err) // Queue up two add funds requests behind create channel @@ -664,7 +732,103 @@ func TestPaychGetMergeAddFunds(t *testing.T) { // Request add funds - should block until create channel has completed var err error - addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1) + addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1, true) + require.NoError(t, err) + }() + + go func() { + defer addFundsSent.Done() + + // Request add funds again - should merge with waiting add funds request + var err error + addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, true) + require.NoError(t, err) + }() + // Wait for add funds requests to be queued up + waitForQueueSize(t, mgr, from, to, 2) + + // Send create channel response + response := testChannelResponse(t, ch) + mock.receiveMsgResponse(createMsgCid, response) + + // Wait for create channel response + chres, err := mgr.GetPaychWaitReady(ctx, createMsgCid) + require.NoError(t, err) + require.Equal(t, ch, chres) + + // Wait for add funds requests to be sent + addFundsSent.Wait() + + // Expect add funds requests to have same channel as create channel and + // same message cid as each other (because they should have been merged) + require.Equal(t, ch, addFundsCh1) + require.Equal(t, ch, addFundsCh2) + require.Equal(t, addFundsMcid1, addFundsMcid2) + + // Send success add funds response + mock.receiveMsgResponse(addFundsMcid1, types.MessageReceipt{ + ExitCode: 0, + Return: []byte{}, + }) + + // Wait for add funds response + addFundsCh, err := mgr.GetPaychWaitReady(ctx, addFundsMcid1) + require.NoError(t, err) + require.Equal(t, ch, addFundsCh) + + // Make sure that one create channel message and one add funds message was + // sent + require.Equal(t, 2, mock.pushedMessageCount()) + + // Check create message amount is correct + createMsg := mock.pushedMessages(createMsgCid) + require.Equal(t, from, createMsg.Message.From) + require.Equal(t, lotusinit.Address, createMsg.Message.To) + require.Equal(t, createAmt, createMsg.Message.Value) + + // Check merged add funds amount is the sum of the individual + // amounts + addFundsMsg := mock.pushedMessages(addFundsMcid1) + require.Equal(t, from, addFundsMsg.Message.From) + require.Equal(t, ch, addFundsMsg.Message.To) + require.Equal(t, types.BigAdd(addFundsAmt1, addFundsAmt2), addFundsMsg.Message.Value) +} + +func TestPaychGetMergePrefundAndReserve(t *testing.T) { + ctx := context.Background() + store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore())) + + ch := tutils.NewIDAddr(t, 100) + from := tutils.NewIDAddr(t, 101) + to := tutils.NewIDAddr(t, 102) + + mock := newMockManagerAPI() + defer mock.close() + + mgr, err := newManager(store, mock) + require.NoError(t, err) + + // Send create message for a channel with value 10 + createAmt := big.NewInt(10) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, true) + require.NoError(t, err) + + // Queue up two add funds requests behind create channel + var addFundsSent sync.WaitGroup + addFundsSent.Add(2) + + addFundsAmt1 := big.NewInt(5) // 1 prefunds + addFundsAmt2 := big.NewInt(3) // 2 reserves + var addFundsCh1 address.Address + var addFundsCh2 address.Address + var addFundsMcid1 cid.Cid + var addFundsMcid2 cid.Cid + go func() { + defer addFundsSent.Done() + + // Request add funds - should block until create channel has completed + var err error + addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1, false) require.NoError(t, err) }() @@ -673,7 +837,7 @@ func TestPaychGetMergeAddFunds(t *testing.T) { // Request add funds again - should merge with waiting add funds request var err error - addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2) + addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, true) require.NoError(t, err) }() // Wait for add funds requests to be queued up @@ -726,6 +890,103 @@ func TestPaychGetMergeAddFunds(t *testing.T) { require.Equal(t, types.BigAdd(addFundsAmt1, addFundsAmt2), addFundsMsg.Message.Value) } +func TestPaychGetMergePrefundAndReservePrefunded(t *testing.T) { + ctx := context.Background() + store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore())) + + ch := tutils.NewIDAddr(t, 100) + from := tutils.NewIDAddr(t, 101) + to := tutils.NewIDAddr(t, 102) + + mock := newMockManagerAPI() + defer mock.close() + + mgr, err := newManager(store, mock) + require.NoError(t, err) + + // Send create message for a channel with value 10 + createAmt := big.NewInt(10) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, false) + require.NoError(t, err) + + // Queue up two add funds requests behind create channel + var addFundsSent sync.WaitGroup + addFundsSent.Add(2) + + addFundsAmt1 := big.NewInt(5) // 1 prefunds + addFundsAmt2 := big.NewInt(3) // 2 reserves + var addFundsCh1 address.Address + var addFundsCh2 address.Address + var addFundsMcid1 cid.Cid + var addFundsMcid2 cid.Cid + go func() { + defer addFundsSent.Done() + + // Request add funds - should block until create channel has completed + var err error + addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1, false) + require.NoError(t, err) + }() + + go func() { + defer addFundsSent.Done() + + // Request add funds again - should merge with waiting add funds request + var err error + addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, true) + require.NoError(t, err) + }() + // Wait for add funds requests to be queued up + waitForQueueSize(t, mgr, from, to, 2) + + // Send create channel response + response := testChannelResponse(t, ch) + mock.receiveMsgResponse(createMsgCid, response) + + // Wait for create channel response + chres, err := mgr.GetPaychWaitReady(ctx, createMsgCid) + require.NoError(t, err) + require.Equal(t, ch, chres) + + // Wait for add funds requests to be sent + addFundsSent.Wait() + + // Expect add funds requests to have same channel as create channel and + // same message cid as each other (because they should have been merged) + require.Equal(t, ch, addFundsCh1) + require.Equal(t, ch, addFundsCh2) + require.NotEqual(t, cid.Undef, addFundsMcid1) + require.Equal(t, cid.Undef, addFundsMcid2) + + // Send success add funds response + mock.receiveMsgResponse(addFundsMcid1, types.MessageReceipt{ + ExitCode: 0, + Return: []byte{}, + }) + + // Wait for add funds response + addFundsCh, err := mgr.GetPaychWaitReady(ctx, addFundsMcid1) + require.NoError(t, err) + require.Equal(t, ch, addFundsCh) + + // Make sure that one create channel message and one add funds message was + // sent + require.Equal(t, 2, mock.pushedMessageCount()) + + // Check create message amount is correct + createMsg := mock.pushedMessages(createMsgCid) + require.Equal(t, from, createMsg.Message.From) + require.Equal(t, lotusinit.Address, createMsg.Message.To) + require.Equal(t, createAmt, createMsg.Message.Value) + + // Check merged add funds amount is the sum of the individual + // amounts + addFundsMsg := mock.pushedMessages(addFundsMcid1) + require.Equal(t, from, addFundsMsg.Message.From) + require.Equal(t, ch, addFundsMsg.Message.To) + require.Equal(t, addFundsAmt1, addFundsMsg.Message.Value) +} + // TestPaychGetMergeAddFundsCtxCancelOne tests that when a queued add funds // request is cancelled, its amount is removed from the total merged add funds func TestPaychGetMergeAddFundsCtxCancelOne(t *testing.T) { @@ -744,7 +1005,7 @@ func TestPaychGetMergeAddFundsCtxCancelOne(t *testing.T) { // Send create message for a channel with value 10 createAmt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, true) require.NoError(t, err) // Queue up two add funds requests behind create channel @@ -761,7 +1022,7 @@ func TestPaychGetMergeAddFundsCtxCancelOne(t *testing.T) { defer addFundsSent.Done() // Request add funds - should block until create channel has completed - _, _, addFundsErr1 = mgr.GetPaych(addFundsCtx1, from, to, addFundsAmt1) + _, _, addFundsErr1 = mgr.GetPaych(addFundsCtx1, from, to, addFundsAmt1, true) }() go func() { @@ -769,7 +1030,7 @@ func TestPaychGetMergeAddFundsCtxCancelOne(t *testing.T) { // Request add funds again - should merge with waiting add funds request var err error - addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2) + addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, true) require.NoError(t, err) }() // Wait for add funds requests to be queued up @@ -841,7 +1102,7 @@ func TestPaychGetMergeAddFundsCtxCancelAll(t *testing.T) { // Send create message for a channel with value 10 createAmt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, true) require.NoError(t, err) // Queue up two add funds requests behind create channel @@ -856,14 +1117,14 @@ func TestPaychGetMergeAddFundsCtxCancelAll(t *testing.T) { defer addFundsSent.Done() // Request add funds - should block until create channel has completed - _, _, addFundsErr1 = mgr.GetPaych(addFundsCtx1, from, to, big.NewInt(5)) + _, _, addFundsErr1 = mgr.GetPaych(addFundsCtx1, from, to, big.NewInt(5), true) }() go func() { defer addFundsSent.Done() // Request add funds again - should merge with waiting add funds request - _, _, addFundsErr2 = mgr.GetPaych(addFundsCtx2, from, to, big.NewInt(3)) + _, _, addFundsErr2 = mgr.GetPaych(addFundsCtx2, from, to, big.NewInt(3), true) }() // Wait for add funds requests to be queued up waitForQueueSize(t, mgr, from, to, 2) @@ -928,7 +1189,7 @@ func TestPaychAvailableFunds(t *testing.T) { // Send create message for a channel with value 10 createAmt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, true) require.NoError(t, err) // Available funds should reflect create channel message sent @@ -953,7 +1214,7 @@ func TestPaychAvailableFunds(t *testing.T) { // Request add funds - should block until create channel has completed var err error - _, addFundsMcid, err = mgr.GetPaych(ctx, from, to, addFundsAmt) + _, addFundsMcid, err = mgr.GetPaych(ctx, from, to, addFundsAmt, true) require.NoError(t, err) }() diff --git a/paychmgr/paychvoucherfunds_test.go b/paychmgr/paychvoucherfunds_test.go index f83a7cd623e..d13a5815eb4 100644 --- a/paychmgr/paychvoucherfunds_test.go +++ b/paychmgr/paychvoucherfunds_test.go @@ -46,7 +46,7 @@ func TestPaychAddVoucherAfterAddFunds(t *testing.T) { // Send create message for a channel with value 10 createAmt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, true) require.NoError(t, err) // Send create channel response @@ -82,7 +82,7 @@ func TestPaychAddVoucherAfterAddFunds(t *testing.T) { require.Equal(t, res.Shortfall, excessAmt) // Add funds so as to cover the voucher shortfall - _, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, excessAmt) + _, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, excessAmt, true) require.NoError(t, err) // Trigger add funds confirmation diff --git a/paychmgr/settle_test.go b/paychmgr/settle_test.go index 43a0062000b..e674d63f4e0 100644 --- a/paychmgr/settle_test.go +++ b/paychmgr/settle_test.go @@ -29,7 +29,7 @@ func TestPaychSettle(t *testing.T) { require.NoError(t, err) amt := big.NewInt(10) - _, mcid, err := mgr.GetPaych(ctx, from, to, amt) + _, mcid, err := mgr.GetPaych(ctx, from, to, amt, true) require.NoError(t, err) // Send channel create response @@ -49,7 +49,7 @@ func TestPaychSettle(t *testing.T) { // (should create a new channel because the previous channel // is settling) amt2 := big.NewInt(5) - _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2) + _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, true) require.NoError(t, err) require.NotEqual(t, cid.Undef, mcid2) diff --git a/paychmgr/simple.go b/paychmgr/simple.go index fd849d3aed7..b9c70a7f3fd 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -109,7 +109,7 @@ func newMergedFundsReq(reqs []*fundsReq) *mergedFundsReq { sort.Slice(m.reqs, func(i, j int) bool { if m.reqs[i].reserve != m.reqs[j].reserve { // non-reserve first - return m.reqs[j].reserve + return m.reqs[i].reserve } // sort by amount asc (reducing latency for smaller requests) @@ -220,50 +220,7 @@ func (ca *channelAccessor) processQueue(ctx context.Context, channelID string) ( return ca.currentAvailableFunds(ctx, channelID, amt) } - { - toReserve := types.BigSub(amt, avail) - avail := types.NewInt(0) - - // reserve at most what we need - ca.mutateChannelInfo(ctx, channelID, func(ci *ChannelInfo) { - avail = ci.AvailableAmount - if avail.GreaterThan(toReserve) { - avail = toReserve - } - ci.AvailableAmount = big.Sub(ci.AvailableAmount, avail) - }) - - used := types.NewInt(0) - - next := 0 - for i, r := range merged.reqs { - if !r.reserve { - // non-reserving request are put after reserving requests, so we are done here - break - } - - if r.amt.GreaterThan(types.BigSub(avail, used)) { - // requests are sorted by amount ascending, so if we hit this, there aren't any more requests we can fill - } - - // don't try to fill inactive requests - if !r.isActive() { - continue - } - - used = types.BigAdd(used, r.amt) - r.onComplete(&paychFundsRes{}) - next = i + 1 - } - merged.reqs = merged.reqs[next:] - - // return any unused reserved funds (e.g. from cancelled requests) - ca.mutateChannelInfo(ctx, channelID, func(ci *ChannelInfo) { - ci.AvailableAmount = types.BigAdd(ci.AvailableAmount, types.BigSub(avail, used)) - }) - } - - res := ca.processTask(merged.ctx, amt, avail) + res := ca.processTask(merged, amt, avail) // If the task is waiting on an external event (eg something to appear on // chain) it will return nil @@ -394,7 +351,9 @@ func (ca *channelAccessor) currentAvailableFunds(ctx context.Context, channelID // Note that processTask may be called repeatedly in the same state, and should // return nil if there is no state change to be made (eg when waiting for a // message to be confirmed on chain) -func (ca *channelAccessor) processTask(ctx context.Context, amt, avail types.BigInt) *paychFundsRes { +func (ca *channelAccessor) processTask(merged *mergedFundsReq, amt, avail types.BigInt) *paychFundsRes { + ctx := merged.ctx + // Get the payment channel for the from/to addresses. // Note: It's ok if we get ErrChannelNotTracked. It just means we need to // create a channel. @@ -427,6 +386,56 @@ func (ca *channelAccessor) processTask(ctx context.Context, amt, avail types.Big return nil } + { + toReserve := types.BigSub(amt, avail) + avail := types.NewInt(0) + + // reserve at most what we need + ca.mutateChannelInfo(ctx, channelInfo.ChannelID, func(ci *ChannelInfo) { + avail = ci.AvailableAmount + if avail.GreaterThan(toReserve) { + avail = toReserve + } + ci.AvailableAmount = big.Sub(ci.AvailableAmount, avail) + }) + + used := types.NewInt(0) + + next := 0 + for i, r := range merged.reqs { + if !r.reserve { + // non-reserving request are put after reserving requests, so we are done here + break + } + + if r.amt.GreaterThan(types.BigSub(avail, used)) { + // requests are sorted by amount ascending, so if we hit this, there aren't any more requests we can fill + break + } + + // don't try to fill inactive requests + if !r.isActive() { + continue + } + + used = types.BigAdd(used, r.amt) + r.onComplete(&paychFundsRes{channel: *channelInfo.Channel}) + next = i + 1 + } + merged.reqs = merged.reqs[next:] + + // return any unused reserved funds (e.g. from cancelled requests) + ca.mutateChannelInfo(ctx, channelInfo.ChannelID, func(ci *ChannelInfo) { + ci.AvailableAmount = types.BigAdd(ci.AvailableAmount, types.BigSub(avail, used)) + }) + + amt = types.BigSub(amt, used) + } + + if amt.LessThanEqual(types.NewInt(0)) { + return nil + } + // We need to add more funds, so send an add funds message to // cover the amount for this request mcid, err := ca.addFunds(ctx, channelInfo, amt, avail) From 2e76375e8aa230a2a2182c0737ec6eb63b3b1896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 4 Jan 2022 21:41:57 +0100 Subject: [PATCH 03/24] Fix paych itests --- itests/paych_api_test.go | 2 +- testplans/lotus-soup/deals_e2e.go | 4 ++-- testplans/lotus-soup/paych/stress.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/itests/paych_api_test.go b/itests/paych_api_test.go index 49c23545b44..fb2233d9043 100644 --- a/itests/paych_api_test.go +++ b/itests/paych_api_test.go @@ -58,7 +58,7 @@ func TestPaymentChannelsAPI(t *testing.T) { require.NoError(t, err) channelAmt := int64(7000) - channelInfo, err := paymentCreator.PaychGet(ctx, createrAddr, receiverAddr, abi.NewTokenAmount(channelAmt)) + channelInfo, err := paymentCreator.PaychGet(ctx, createrAddr, receiverAddr, abi.NewTokenAmount(channelAmt), true) require.NoError(t, err) channel, err := paymentCreator.PaychGetWaitReady(ctx, channelInfo.WaitSentinel) diff --git a/testplans/lotus-soup/deals_e2e.go b/testplans/lotus-soup/deals_e2e.go index 6737bdae226..bc970350837 100644 --- a/testplans/lotus-soup/deals_e2e.go +++ b/testplans/lotus-soup/deals_e2e.go @@ -207,7 +207,7 @@ func initPaymentChannel(t *testkit.TestEnvironment, ctx context.Context, cl *tes t.RecordMessage("my balance: %d", balance) t.RecordMessage("creating payment channel; from=%s, to=%s, funds=%d", cl.Wallet.Address, recv.WalletAddr, balance) - channel, err := cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, balance) + channel, err := cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, balance, true) if err != nil { return fmt.Errorf("failed to create payment channel: %w", err) } @@ -230,7 +230,7 @@ func initPaymentChannel(t *testkit.TestEnvironment, ctx context.Context, cl *tes // we wait for 2 confirmations, so we have the assurance the channel is tracked. t.RecordMessage("reloading paych; now it should have an address") - channel, err = cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, big.Zero()) + channel, err = cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, big.Zero(), true) if err != nil { return fmt.Errorf("failed to reload payment channel: %w", err) } diff --git a/testplans/lotus-soup/paych/stress.go b/testplans/lotus-soup/paych/stress.go index 85246603f0f..e0d324f03e9 100644 --- a/testplans/lotus-soup/paych/stress.go +++ b/testplans/lotus-soup/paych/stress.go @@ -124,7 +124,7 @@ func runSender(ctx context.Context, t *testkit.TestEnvironment, clients []*testk time.Sleep(20 * time.Second) - channel, err := cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, channelAmt) + channel, err := cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, channelAmt, true) if err != nil { return fmt.Errorf("failed to create payment channel: %w", err) } From 7a938b2dea63793c35598e239385ad0a46baef21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 4 Jan 2022 22:53:59 +0100 Subject: [PATCH 04/24] paych: Output FIL in cli --- cli/paych.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/paych.go b/cli/paych.go index 1d5e304c383..03fd1cd6e5b 100644 --- a/cli/paych.go +++ b/cli/paych.go @@ -169,7 +169,7 @@ func paychStatus(writer io.Writer, avail *api.ChannelAvailableFunds) { fmt.Fprint(writer, "Creating channel\n") fmt.Fprintf(writer, " From: %s\n", avail.From) fmt.Fprintf(writer, " To: %s\n", avail.To) - fmt.Fprintf(writer, " Pending Amt: %d\n", avail.PendingAmt) + fmt.Fprintf(writer, " Pending Amt: %s\n", types.FIL(avail.PendingAmt)) fmt.Fprintf(writer, " Wait Sentinel: %s\n", avail.PendingWaitSentinel) return } @@ -189,10 +189,10 @@ func paychStatus(writer io.Writer, avail *api.ChannelAvailableFunds) { {"Channel", avail.Channel.String()}, {"From", avail.From.String()}, {"To", avail.To.String()}, - {"Confirmed Amt", fmt.Sprintf("%d", avail.ConfirmedAmt)}, - {"Pending Amt", fmt.Sprintf("%d", avail.PendingAmt)}, - {"Queued Amt", fmt.Sprintf("%d", avail.QueuedAmt)}, - {"Voucher Redeemed Amt", fmt.Sprintf("%d", avail.VoucherReedeemedAmt)}, + {"Confirmed Amt", fmt.Sprintf("%s", types.FIL(avail.ConfirmedAmt))}, + {"Pending Amt", fmt.Sprintf("%s", types.FIL(avail.PendingAmt))}, + {"Queued Amt", fmt.Sprintf("%s", types.FIL(avail.QueuedAmt))}, + {"Voucher Redeemed Amt", fmt.Sprintf("%s", types.FIL(avail.VoucherReedeemedAmt))}, } if avail.PendingWaitSentinel != nil { nameValues = append(nameValues, []string{ @@ -576,7 +576,7 @@ func outputVoucher(w io.Writer, v *paych.SignedVoucher, export bool) error { } } - fmt.Fprintf(w, "Lane %d, Nonce %d: %s", v.Lane, v.Nonce, v.Amount.String()) + fmt.Fprintf(w, "Lane %d, Nonce %d: %s", v.Lane, v.Nonce, types.FIL(v.Amount)) if export { fmt.Fprintf(w, "; %s", enc) } From 8f6f21c94cee6b3bc0c596ee87bbd5b21ee45edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 4 Jan 2022 23:20:11 +0100 Subject: [PATCH 05/24] paych: Print available amounts in paych status --- api/api_full.go | 4 ++++ cli/paych.go | 2 ++ paychmgr/manager.go | 2 ++ paychmgr/simple.go | 2 ++ 4 files changed, 10 insertions(+) diff --git a/api/api_full.go b/api/api_full.go index cf9a1f22ef2..47f05fb7976 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -854,6 +854,10 @@ type ChannelAvailableFunds struct { ConfirmedAmt types.BigInt // PendingAmt is the amount of funds that are pending confirmation on-chain PendingAmt types.BigInt + // AvailableAmt is part of ConfirmedAmt that is available for use (pre-allocated) + AvailableAmt types.BigInt + // PendingAvailableAmt is the amount of available funds that are pending confirmation on-chain + PendingAvailableAmt types.BigInt // PendingWaitSentinel can be used with PaychGetWaitReady to wait for // confirmation of pending funds PendingWaitSentinel *cid.Cid diff --git a/cli/paych.go b/cli/paych.go index 03fd1cd6e5b..eef26227231 100644 --- a/cli/paych.go +++ b/cli/paych.go @@ -191,6 +191,8 @@ func paychStatus(writer io.Writer, avail *api.ChannelAvailableFunds) { {"To", avail.To.String()}, {"Confirmed Amt", fmt.Sprintf("%s", types.FIL(avail.ConfirmedAmt))}, {"Pending Amt", fmt.Sprintf("%s", types.FIL(avail.PendingAmt))}, + {"Available Amt", fmt.Sprintf("%s", types.FIL(avail.AvailableAmt))}, + {"Pending Available Amt", fmt.Sprintf("%s", types.FIL(avail.PendingAvailableAmt))}, {"Queued Amt", fmt.Sprintf("%s", types.FIL(avail.QueuedAmt))}, {"Voucher Redeemed Amt", fmt.Sprintf("%s", types.FIL(avail.VoucherReedeemedAmt))}, } diff --git a/paychmgr/manager.go b/paychmgr/manager.go index eed475547aa..612cb6678a8 100644 --- a/paychmgr/manager.go +++ b/paychmgr/manager.go @@ -142,6 +142,8 @@ func (pm *Manager) AvailableFundsByFromTo(ctx context.Context, from address.Addr To: to, ConfirmedAmt: types.NewInt(0), PendingAmt: types.NewInt(0), + AvailableAmt: types.NewInt(0), + PendingAvailableAmt: types.NewInt(0), PendingWaitSentinel: nil, QueuedAmt: types.NewInt(0), VoucherReedeemedAmt: types.NewInt(0), diff --git a/paychmgr/simple.go b/paychmgr/simple.go index b9c70a7f3fd..45a522ba41d 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -340,6 +340,8 @@ func (ca *channelAccessor) currentAvailableFunds(ctx context.Context, channelID To: channelInfo.to(), ConfirmedAmt: channelInfo.Amount, PendingAmt: channelInfo.PendingAmount, + AvailableAmt: channelInfo.AvailableAmount, + PendingAvailableAmt: channelInfo.PendingAvailableAmount, PendingWaitSentinel: waitSentinel, QueuedAmt: queuedAmt, VoucherReedeemedAmt: totalRedeemed, From ff8b95df934264f9c85bb0ecb82bb7cf7b277176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 4 Jan 2022 23:31:22 +0100 Subject: [PATCH 06/24] paych: Reserve flag for add-funds cli --- cli/paych.go | 9 ++++++--- paychmgr/store.go | 6 ++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/cli/paych.go b/cli/paych.go index eef26227231..171ab38abfc 100644 --- a/cli/paych.go +++ b/cli/paych.go @@ -39,12 +39,15 @@ var paychAddFundsCmd = &cli.Command{ Usage: "Add funds to the payment channel between fromAddress and toAddress. Creates the payment channel if it doesn't already exist.", ArgsUsage: "[fromAddress toAddress amount]", Flags: []cli.Flag{ - &cli.BoolFlag{ Name: "restart-retrievals", Usage: "restart stalled retrieval deals on this payment channel", Value: true, }, + &cli.BoolFlag{ + Name: "reserve", + Usage: "mark funds as reserved", + }, }, Action: func(cctx *cli.Context) error { if cctx.Args().Len() != 3 { @@ -66,7 +69,7 @@ var paychAddFundsCmd = &cli.Command{ return ShowHelp(cctx, fmt.Errorf("parsing amount failed: %s", err)) } - api, closer, err := GetFullNodeAPI(cctx) + api, closer, err := GetFullNodeAPIV1(cctx) if err != nil { return err } @@ -76,7 +79,7 @@ var paychAddFundsCmd = &cli.Command{ // Send a message to chain to create channel / add funds to existing // channel - info, err := api.PaychGet(ctx, from, to, types.BigInt(amt)) + info, err := api.PaychGet(ctx, from, to, types.BigInt(amt), cctx.Bool("reserve")) if err != nil { return err } diff --git a/paychmgr/store.go b/paychmgr/store.go index bbc549b862c..d5c8e198049 100644 --- a/paychmgr/store.go +++ b/paychmgr/store.go @@ -502,5 +502,11 @@ func unmarshallChannelInfo(stored *ChannelInfo, value []byte) (*ChannelInfo, err stored.Channel = nil } + // backwards compat + if stored.AvailableAmount.Int == nil { + stored.AvailableAmount = types.NewInt(0) + stored.PendingAvailableAmount = types.NewInt(0) + } + return stored, nil } From 1f2621b57430e024910a33bf6da3d19c92fdcdb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Wed, 5 Jan 2022 00:09:19 +0100 Subject: [PATCH 07/24] Make retrieval work with reused channels --- markets/retrievaladapter/client.go | 34 ++++++++++++++++++++++++++++++ paychmgr/simple.go | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/markets/retrievaladapter/client.go b/markets/retrievaladapter/client.go index 4ed2e905ab4..88fcb7ba1e3 100644 --- a/markets/retrievaladapter/client.go +++ b/markets/retrievaladapter/client.go @@ -9,6 +9,8 @@ import ( "github.com/filecoin-project/go-state-types/abi" "github.com/ipfs/go-cid" "github.com/multiformats/go-multiaddr" + mh "github.com/multiformats/go-multihash" + "golang.org/x/xerrors" "github.com/filecoin-project/lotus/chain/actors/builtin/paych" "github.com/filecoin-project/lotus/chain/types" @@ -16,6 +18,27 @@ import ( payapi "github.com/filecoin-project/lotus/node/impl/paych" ) +func mkPaychReusedCid(addr address.Address) cid.Cid { + c, err := cid.V1Builder{Codec: cid.Raw, MhType: mh.IDENTITY}.Sum(addr.Bytes()) + if err != nil { + panic(err) + } + return c +} + +func extractPaychReusedCid(c cid.Cid) (address.Address, error) { + if c.Prefix().Codec != cid.Raw { + return address.Undef, nil + } + + h, err := mh.Decode(c.Hash()) + if err != nil { + return address.Address{}, err + } + + return address.NewFromBytes(h.Digest) +} + type retrievalClientNode struct { chainAPI full.ChainAPI payAPI payapi.PaychAPI @@ -38,6 +61,10 @@ func (rcn *retrievalClientNode) GetOrCreatePaymentChannel(ctx context.Context, c if err != nil { return address.Undef, cid.Undef, err } + if ci.WaitSentinel == cid.Undef { + return ci.Channel, mkPaychReusedCid(ci.Channel), nil + } + return ci.Channel, ci.WaitSentinel, nil } @@ -74,6 +101,13 @@ func (rcn *retrievalClientNode) GetChainHead(ctx context.Context) (shared.TipSet } func (rcn *retrievalClientNode) WaitForPaymentChannelReady(ctx context.Context, messageCID cid.Cid) (address.Address, error) { + maybeAddr, err := extractPaychReusedCid(messageCID) + if err != nil { + return address.Address{}, xerrors.Errorf("extract paych reused CID: %w", err) + } + if maybeAddr != address.Undef { + return maybeAddr, nil + } return rcn.payAPI.PaychGetWaitReady(ctx, messageCID) } diff --git a/paychmgr/simple.go b/paychmgr/simple.go index 45a522ba41d..d06cb870df4 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -41,7 +41,7 @@ type fundsReq struct { } func newFundsReq(ctx context.Context, amt types.BigInt, reserve bool) *fundsReq { - promise := make(chan *paychFundsRes) + promise := make(chan *paychFundsRes, 1) return &fundsReq{ ctx: ctx, promise: promise, From eab8fecd2631766e29a5e1f7bce8d3eac858f321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Wed, 5 Jan 2022 00:17:03 +0100 Subject: [PATCH 08/24] gen stuff --- documentation/en/api-v0-methods.md | 4 ++++ documentation/en/api-v1-unstable-methods.md | 4 ++++ documentation/en/cli-lotus.md | 1 + 3 files changed, 9 insertions(+) diff --git a/documentation/en/api-v0-methods.md b/documentation/en/api-v0-methods.md index 883d4d27499..2061d7fadb7 100644 --- a/documentation/en/api-v0-methods.md +++ b/documentation/en/api-v0-methods.md @@ -4061,6 +4061,8 @@ Response: "To": "f01234", "ConfirmedAmt": "0", "PendingAmt": "0", + "AvailableAmt": "0", + "PendingAvailableAmt": "0", "PendingWaitSentinel": null, "QueuedAmt": "0", "VoucherReedeemedAmt": "0" @@ -4088,6 +4090,8 @@ Response: "To": "f01234", "ConfirmedAmt": "0", "PendingAmt": "0", + "AvailableAmt": "0", + "PendingAvailableAmt": "0", "PendingWaitSentinel": null, "QueuedAmt": "0", "VoucherReedeemedAmt": "0" diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index ce0ceadcdfd..b84877b97a5 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -4456,6 +4456,8 @@ Response: "To": "f01234", "ConfirmedAmt": "0", "PendingAmt": "0", + "AvailableAmt": "0", + "PendingAvailableAmt": "0", "PendingWaitSentinel": null, "QueuedAmt": "0", "VoucherReedeemedAmt": "0" @@ -4483,6 +4485,8 @@ Response: "To": "f01234", "ConfirmedAmt": "0", "PendingAmt": "0", + "AvailableAmt": "0", + "PendingAvailableAmt": "0", "PendingWaitSentinel": null, "QueuedAmt": "0", "VoucherReedeemedAmt": "0" diff --git a/documentation/en/cli-lotus.md b/documentation/en/cli-lotus.md index 54c0d36dfe6..46e481b0879 100644 --- a/documentation/en/cli-lotus.md +++ b/documentation/en/cli-lotus.md @@ -1347,6 +1347,7 @@ USAGE: OPTIONS: --restart-retrievals restart stalled retrieval deals on this payment channel (default: true) + --reserve mark funds as reserved (default: false) --help, -h show help (default: false) ``` From 533349cc0d0d13de84531e2deb822b2f3a51a210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Wed, 5 Jan 2022 12:11:19 +0100 Subject: [PATCH 09/24] paych: Fix cli tests --- cli/paych.go | 4 ++-- itests/paych_cli_test.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/paych.go b/cli/paych.go index 171ab38abfc..29e8c640719 100644 --- a/cli/paych.go +++ b/cli/paych.go @@ -193,11 +193,11 @@ func paychStatus(writer io.Writer, avail *api.ChannelAvailableFunds) { {"From", avail.From.String()}, {"To", avail.To.String()}, {"Confirmed Amt", fmt.Sprintf("%s", types.FIL(avail.ConfirmedAmt))}, - {"Pending Amt", fmt.Sprintf("%s", types.FIL(avail.PendingAmt))}, {"Available Amt", fmt.Sprintf("%s", types.FIL(avail.AvailableAmt))}, + {"Voucher Redeemed Amt", fmt.Sprintf("%s", types.FIL(avail.VoucherReedeemedAmt))}, + {"Pending Amt", fmt.Sprintf("%s", types.FIL(avail.PendingAmt))}, {"Pending Available Amt", fmt.Sprintf("%s", types.FIL(avail.PendingAvailableAmt))}, {"Queued Amt", fmt.Sprintf("%s", types.FIL(avail.QueuedAmt))}, - {"Voucher Redeemed Amt", fmt.Sprintf("%s", types.FIL(avail.VoucherReedeemedAmt))}, } if avail.PendingWaitSentinel != nil { nameValues = append(nameValues, []string{ diff --git a/itests/paych_cli_test.go b/itests/paych_cli_test.go index a4ad1920b6e..f964d781384 100644 --- a/itests/paych_cli_test.go +++ b/itests/paych_cli_test.go @@ -133,10 +133,10 @@ func TestPaymentChannelStatus(t *testing.T) { require.True(t, stateCreating || stateCreated) channelAmtAtto := types.BigMul(types.NewInt(channelAmt), types.NewInt(build.FilecoinPrecision)) - channelAmtStr := fmt.Sprintf("%d", channelAmtAtto) + channelAmtStr := fmt.Sprintf("%s", types.FIL(channelAmtAtto)) if stateCreating { // If we're in the creating state (most likely) the amount should be pending - require.Regexp(t, regexp.MustCompile("Pending.*"+channelAmtStr), out) + require.Regexp(t, regexp.MustCompile("Pending Amt.*"+channelAmtStr), out) } // Wait for create channel to complete @@ -159,7 +159,7 @@ func TestPaymentChannelStatus(t *testing.T) { out = creatorCLI.RunCmd("paych", "status", chstr) fmt.Println(out) voucherAmtAtto := types.BigMul(types.NewInt(voucherAmt), types.NewInt(build.FilecoinPrecision)) - voucherAmtStr := fmt.Sprintf("%d", voucherAmtAtto) + voucherAmtStr := fmt.Sprintf("%s", types.FIL(voucherAmtAtto)) // Output should include voucher amount require.Regexp(t, regexp.MustCompile("Voucher.*"+voucherAmtStr), out) } From b0e7bc15c2f664dc96053ed4f70c61e3421bfe68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Wed, 5 Jan 2022 16:11:32 +0100 Subject: [PATCH 10/24] paych: Cleanup available fund logic --- paychmgr/simple.go | 100 +++++++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/paychmgr/simple.go b/paychmgr/simple.go index d06cb870df4..747eac58509 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -163,6 +163,35 @@ func (m *mergedFundsReq) sum() (types.BigInt, types.BigInt) { return sum, avail } +// completeAmount completes first non-reserving requests up to the available amount +func (m *mergedFundsReq) completeAmount(avail types.BigInt, channelInfo *ChannelInfo) types.BigInt { + used := types.NewInt(0) + next := 0 + for i, r := range m.reqs { + if !r.reserve { + // non-reserving request are put after reserving requests, so we are done here + break + } + + if r.amt.GreaterThan(types.BigSub(avail, used)) { + // requests are sorted by amount ascending, so if we hit this, there aren't any more requests we can fill + break + } + + // don't try to fill inactive requests + if !r.isActive() { + continue + } + + used = types.BigAdd(used, r.amt) + r.onComplete(&paychFundsRes{channel: *channelInfo.Channel}) + next = i + 1 + } + + m.reqs = m.reqs[next:] + return used +} + // getPaych ensures that a channel exists between the from and to addresses, // and reserves (or adds as available) the given amount of funds. // If the channel does not exist a create channel message is sent and the @@ -388,51 +417,8 @@ func (ca *channelAccessor) processTask(merged *mergedFundsReq, amt, avail types. return nil } - { - toReserve := types.BigSub(amt, avail) - avail := types.NewInt(0) - - // reserve at most what we need - ca.mutateChannelInfo(ctx, channelInfo.ChannelID, func(ci *ChannelInfo) { - avail = ci.AvailableAmount - if avail.GreaterThan(toReserve) { - avail = toReserve - } - ci.AvailableAmount = big.Sub(ci.AvailableAmount, avail) - }) - - used := types.NewInt(0) - - next := 0 - for i, r := range merged.reqs { - if !r.reserve { - // non-reserving request are put after reserving requests, so we are done here - break - } - - if r.amt.GreaterThan(types.BigSub(avail, used)) { - // requests are sorted by amount ascending, so if we hit this, there aren't any more requests we can fill - break - } - - // don't try to fill inactive requests - if !r.isActive() { - continue - } - - used = types.BigAdd(used, r.amt) - r.onComplete(&paychFundsRes{channel: *channelInfo.Channel}) - next = i + 1 - } - merged.reqs = merged.reqs[next:] - - // return any unused reserved funds (e.g. from cancelled requests) - ca.mutateChannelInfo(ctx, channelInfo.ChannelID, func(ci *ChannelInfo) { - ci.AvailableAmount = types.BigAdd(ci.AvailableAmount, types.BigSub(avail, used)) - }) - - amt = types.BigSub(amt, used) - } + // Try to fill requests using available funds, without going to the chain + amt = ca.completeAvailable(ctx, merged, channelInfo, amt, avail) if amt.LessThanEqual(types.NewInt(0)) { return nil @@ -533,6 +519,30 @@ func (ca *channelAccessor) waitPaychCreateMsg(ctx context.Context, channelID str return nil } +// completeAvailable fills reserving fund requests using already available funds, without interacting with the chain +func (ca *channelAccessor) completeAvailable(ctx context.Context, merged *mergedFundsReq, channelInfo *ChannelInfo, amt, av types.BigInt) types.BigInt { + toReserve := types.BigSub(amt, av) + avail := types.NewInt(0) + + // reserve at most what we need + ca.mutateChannelInfo(ctx, channelInfo.ChannelID, func(ci *ChannelInfo) { + avail = ci.AvailableAmount + if avail.GreaterThan(toReserve) { + avail = toReserve + } + ci.AvailableAmount = big.Sub(ci.AvailableAmount, avail) + }) + + used := merged.completeAmount(avail, channelInfo) + + // return any unused reserved funds (e.g. from cancelled requests) + ca.mutateChannelInfo(ctx, channelInfo.ChannelID, func(ci *ChannelInfo) { + ci.AvailableAmount = types.BigAdd(ci.AvailableAmount, types.BigSub(avail, used)) + }) + + return types.BigSub(amt, used) +} + // addFunds sends a message to add funds to the channel and returns the message cid func (ca *channelAccessor) addFunds(ctx context.Context, channelInfo *ChannelInfo, amt, avail types.BigInt) (*cid.Cid, error) { msg := &types.Message{ From 5b585c0285f22d26d8c50db909b12cf2cd479f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Wed, 5 Jan 2022 21:49:26 +0100 Subject: [PATCH 11/24] paych: reset fundsReqQueue correctly --- paychmgr/simple.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/paychmgr/simple.go b/paychmgr/simple.go index 747eac58509..def24d1cf5c 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -164,7 +164,7 @@ func (m *mergedFundsReq) sum() (types.BigInt, types.BigInt) { } // completeAmount completes first non-reserving requests up to the available amount -func (m *mergedFundsReq) completeAmount(avail types.BigInt, channelInfo *ChannelInfo) types.BigInt { +func (m *mergedFundsReq) completeAmount(avail types.BigInt, channelInfo *ChannelInfo) (*paychFundsRes, types.BigInt) { used := types.NewInt(0) next := 0 for i, r := range m.reqs { @@ -189,7 +189,10 @@ func (m *mergedFundsReq) completeAmount(avail types.BigInt, channelInfo *Channel } m.reqs = m.reqs[next:] - return used + if len(m.reqs) == 0 { + return &paychFundsRes{channel: *channelInfo.Channel}, used + } + return nil, used } // getPaych ensures that a channel exists between the from and to addresses, @@ -418,10 +421,10 @@ func (ca *channelAccessor) processTask(merged *mergedFundsReq, amt, avail types. } // Try to fill requests using available funds, without going to the chain - amt = ca.completeAvailable(ctx, merged, channelInfo, amt, avail) + res, amt := ca.completeAvailable(ctx, merged, channelInfo, amt, avail) - if amt.LessThanEqual(types.NewInt(0)) { - return nil + if res != nil || amt.LessThanEqual(types.NewInt(0)) { + return res } // We need to add more funds, so send an add funds message to @@ -520,7 +523,7 @@ func (ca *channelAccessor) waitPaychCreateMsg(ctx context.Context, channelID str } // completeAvailable fills reserving fund requests using already available funds, without interacting with the chain -func (ca *channelAccessor) completeAvailable(ctx context.Context, merged *mergedFundsReq, channelInfo *ChannelInfo, amt, av types.BigInt) types.BigInt { +func (ca *channelAccessor) completeAvailable(ctx context.Context, merged *mergedFundsReq, channelInfo *ChannelInfo, amt, av types.BigInt) (*paychFundsRes, types.BigInt) { toReserve := types.BigSub(amt, av) avail := types.NewInt(0) @@ -533,14 +536,14 @@ func (ca *channelAccessor) completeAvailable(ctx context.Context, merged *merged ci.AvailableAmount = big.Sub(ci.AvailableAmount, avail) }) - used := merged.completeAmount(avail, channelInfo) + res, used := merged.completeAmount(avail, channelInfo) // return any unused reserved funds (e.g. from cancelled requests) ca.mutateChannelInfo(ctx, channelInfo.ChannelID, func(ci *ChannelInfo) { ci.AvailableAmount = types.BigAdd(ci.AvailableAmount, types.BigSub(avail, used)) }) - return types.BigSub(amt, used) + return res, types.BigSub(amt, used) } // addFunds sends a message to add funds to the channel and returns the message cid From 8b19b84140ee432eb5ea0c93f8c5a8c1dd5dbf19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Thu, 6 Jan 2022 16:04:39 +0100 Subject: [PATCH 12/24] paych: option to force off-chain get --- api/api_full.go | 15 +- api/mocks/mock_full.go | 2 +- api/proxy_gen.go | 6 +- api/v0api/v1_wrapper.go | 6 +- cli/paych.go | 9 +- documentation/en/api-v1-unstable-methods.md | 13 +- itests/paych_api_test.go | 5 +- markets/retrievaladapter/client.go | 6 +- node/impl/paych/paych.go | 9 +- paychmgr/manager.go | 8 +- paychmgr/paychget_test.go | 369 ++++++++++++++++++-- paychmgr/paychvoucherfunds_test.go | 4 +- paychmgr/settle_test.go | 4 +- paychmgr/simple.go | 78 ++++- testplans/lotus-soup/deals_e2e.go | 10 +- testplans/lotus-soup/paych/stress.go | 5 +- 16 files changed, 468 insertions(+), 81 deletions(-) diff --git a/api/api_full.go b/api/api_full.go index 47f05fb7976..853d4928390 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -690,10 +690,14 @@ type FullNode interface { // The Paych methods are for interacting with and managing payment channels // PaychGet gets or creates a payment channel between address pair - // - If reserve is false, the specified amount will be added to the channel through on-chain send for future use - // - If reserve is true, the specified amount will be reserved for use. If there aren't enough non-reserved funds + // - If opts.Reserve is false, the specified amount will be added to the channel through on-chain send for future use + // - If opts.Reserve is true, the specified amount will be reserved for use. If there aren't enough non-reserved funds // available, funds will be added through an on-chain message. - PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt, reserve bool) (*ChannelInfo, error) //perm:sign + // - When opts.OffChain is true, this call will not cause any messages to be sent to the chain (no automatic + // channel creation/funds adding). If the operation can't be performed without sending a message an error will be + // returned. Note that even when this option is specified, this call can be blocked by previous operations on the + // channel waiting for on-chain operations. + PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt, opts PaychGetOpts) (*ChannelInfo, error) //perm:sign PaychGetWaitReady(context.Context, cid.Cid) (address.Address, error) //perm:sign PaychAvailableFunds(ctx context.Context, ch address.Address) (*ChannelAvailableFunds, error) //perm:sign PaychAvailableFundsByFromTo(ctx context.Context, from, to address.Address) (*ChannelAvailableFunds, error) //perm:sign @@ -832,6 +836,11 @@ const ( PCHOutbound ) +type PaychGetOpts struct { + Reserve bool + OffChain bool +} + type PaychStatus struct { ControlAddr address.Address Direction PCHDir diff --git a/api/mocks/mock_full.go b/api/mocks/mock_full.go index c745f26fcd4..5d42ba3c918 100644 --- a/api/mocks/mock_full.go +++ b/api/mocks/mock_full.go @@ -1976,7 +1976,7 @@ func (mr *MockFullNodeMockRecorder) PaychCollect(arg0, arg1 interface{}) *gomock } // PaychGet mocks base method. -func (m *MockFullNode) PaychGet(arg0 context.Context, arg1, arg2 address.Address, arg3 big.Int, arg4 bool) (*api.ChannelInfo, error) { +func (m *MockFullNode) PaychGet(arg0 context.Context, arg1, arg2 address.Address, arg3 big.Int, arg4 api.PaychGetOpts) (*api.ChannelInfo, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PaychGet", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(*api.ChannelInfo) diff --git a/api/proxy_gen.go b/api/proxy_gen.go index 9a134cec91b..9615181f58c 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -306,7 +306,7 @@ type FullNodeStruct struct { PaychCollect func(p0 context.Context, p1 address.Address) (cid.Cid, error) `perm:"sign"` - PaychGet func(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt, p4 bool) (*ChannelInfo, error) `perm:"sign"` + PaychGet func(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt, p4 PaychGetOpts) (*ChannelInfo, error) `perm:"sign"` PaychGetWaitReady func(p0 context.Context, p1 cid.Cid) (address.Address, error) `perm:"sign"` @@ -2179,14 +2179,14 @@ func (s *FullNodeStub) PaychCollect(p0 context.Context, p1 address.Address) (cid return *new(cid.Cid), ErrNotSupported } -func (s *FullNodeStruct) PaychGet(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt, p4 bool) (*ChannelInfo, error) { +func (s *FullNodeStruct) PaychGet(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt, p4 PaychGetOpts) (*ChannelInfo, error) { if s.Internal.PaychGet == nil { return nil, ErrNotSupported } return s.Internal.PaychGet(p0, p1, p2, p3, p4) } -func (s *FullNodeStub) PaychGet(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt, p4 bool) (*ChannelInfo, error) { +func (s *FullNodeStub) PaychGet(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt, p4 PaychGetOpts) (*ChannelInfo, error) { return nil, ErrNotSupported } diff --git a/api/v0api/v1_wrapper.go b/api/v0api/v1_wrapper.go index 605b27b0c9f..1c22eb920ad 100644 --- a/api/v0api/v1_wrapper.go +++ b/api/v0api/v1_wrapper.go @@ -338,8 +338,10 @@ func (w *WrapperV1Full) clientRetrieve(ctx context.Context, order RetrievalOrder } func (w *WrapperV1Full) PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt) (*api.ChannelInfo, error) { - // v0 always reserves - return w.FullNode.PaychGet(ctx, from, to, amt, true) + return w.FullNode.PaychGet(ctx, from, to, amt, api.PaychGetOpts{ + Reserve: true, // v0 always reserves + OffChain: false, + }) } var _ FullNode = &WrapperV1Full{} diff --git a/cli/paych.go b/cli/paych.go index 29e8c640719..78e661d155f 100644 --- a/cli/paych.go +++ b/cli/paych.go @@ -8,7 +8,7 @@ import ( "sort" "strings" - "github.com/filecoin-project/lotus/api" + lapi "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/paychmgr" @@ -79,7 +79,10 @@ var paychAddFundsCmd = &cli.Command{ // Send a message to chain to create channel / add funds to existing // channel - info, err := api.PaychGet(ctx, from, to, types.BigInt(amt), cctx.Bool("reserve")) + info, err := api.PaychGet(ctx, from, to, types.BigInt(amt), lapi.PaychGetOpts{ + Reserve: cctx.Bool("reserve"), + OffChain: false, + }) if err != nil { return err } @@ -166,7 +169,7 @@ var paychStatusCmd = &cli.Command{ }, } -func paychStatus(writer io.Writer, avail *api.ChannelAvailableFunds) { +func paychStatus(writer io.Writer, avail *lapi.ChannelAvailableFunds) { if avail.Channel == nil { if avail.PendingWaitSentinel != nil { fmt.Fprint(writer, "Creating channel\n") diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index b84877b97a5..2926faab7c8 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -4514,9 +4514,13 @@ Response: ### PaychGet PaychGet gets or creates a payment channel between address pair - - If reserve is false, the specified amount will be added to the channel through on-chain send for future use - - If reserve is true, the specified amount will be reserved for use. If there aren't enough non-reserved funds + - If opts.Reserve is false, the specified amount will be added to the channel through on-chain send for future use + - If opts.Reserve is true, the specified amount will be reserved for use. If there aren't enough non-reserved funds available, funds will be added through an on-chain message. + - When opts.OffChain is true, this call will not cause any messages to be sent to the chain (no automatic + channel creation/funds adding). If the operation can't be performed without sending a message an error will be + returned. Note that even when this option is specified, this call can be blocked by previous operations on the + channel waiting for on-chain operations. Perms: sign @@ -4527,7 +4531,10 @@ Inputs: "f01234", "f01234", "0", - true + { + "Reserve": true, + "OffChain": true + } ] ``` diff --git a/itests/paych_api_test.go b/itests/paych_api_test.go index fb2233d9043..c2d14aeb8cb 100644 --- a/itests/paych_api_test.go +++ b/itests/paych_api_test.go @@ -58,7 +58,10 @@ func TestPaymentChannelsAPI(t *testing.T) { require.NoError(t, err) channelAmt := int64(7000) - channelInfo, err := paymentCreator.PaychGet(ctx, createrAddr, receiverAddr, abi.NewTokenAmount(channelAmt), true) + channelInfo, err := paymentCreator.PaychGet(ctx, createrAddr, receiverAddr, abi.NewTokenAmount(channelAmt), api.PaychGetOpts{ + Reserve: true, + OffChain: false, + }) require.NoError(t, err) channel, err := paymentCreator.PaychGetWaitReady(ctx, channelInfo.WaitSentinel) diff --git a/markets/retrievaladapter/client.go b/markets/retrievaladapter/client.go index 88fcb7ba1e3..601f9f2550f 100644 --- a/markets/retrievaladapter/client.go +++ b/markets/retrievaladapter/client.go @@ -12,6 +12,7 @@ import ( mh "github.com/multiformats/go-multihash" "golang.org/x/xerrors" + "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/actors/builtin/paych" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/node/impl/full" @@ -57,7 +58,10 @@ func NewRetrievalClientNode(payAPI payapi.PaychAPI, chainAPI full.ChainAPI, stat func (rcn *retrievalClientNode) GetOrCreatePaymentChannel(ctx context.Context, clientAddress address.Address, minerAddress address.Address, clientFundsAvailable abi.TokenAmount, tok shared.TipSetToken) (address.Address, cid.Cid, error) { // TODO: respect the provided TipSetToken (a serialized TipSetKey) when // querying the chain - ci, err := rcn.payAPI.PaychGet(ctx, clientAddress, minerAddress, clientFundsAvailable, true) + ci, err := rcn.payAPI.PaychGet(ctx, clientAddress, minerAddress, clientFundsAvailable, api.PaychGetOpts{ + Reserve: true, + OffChain: false, + }) if err != nil { return address.Undef, cid.Undef, err } diff --git a/node/impl/paych/paych.go b/node/impl/paych/paych.go index d308f6248ac..2d8777dbb9a 100644 --- a/node/impl/paych/paych.go +++ b/node/impl/paych/paych.go @@ -22,8 +22,8 @@ type PaychAPI struct { PaychMgr *paychmgr.Manager } -func (a *PaychAPI) PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt, reserve bool) (*api.ChannelInfo, error) { - ch, mcid, err := a.PaychMgr.GetPaych(ctx, from, to, amt, reserve) +func (a *PaychAPI) PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt, opts api.PaychGetOpts) (*api.ChannelInfo, error) { + ch, mcid, err := a.PaychMgr.GetPaych(ctx, from, to, amt, opts) if err != nil { return nil, err } @@ -55,7 +55,10 @@ func (a *PaychAPI) PaychNewPayment(ctx context.Context, from, to address.Address // TODO: Fix free fund tracking in PaychGet // TODO: validate voucher spec before locking funds - ch, err := a.PaychGet(ctx, from, to, amount, true) + ch, err := a.PaychGet(ctx, from, to, amount, api.PaychGetOpts{ + Reserve: true, + OffChain: false, + }) if err != nil { return nil, err } diff --git a/paychmgr/manager.go b/paychmgr/manager.go index 612cb6678a8..7045a7dcd23 100644 --- a/paychmgr/manager.go +++ b/paychmgr/manager.go @@ -101,13 +101,17 @@ func (pm *Manager) Stop() error { return nil } -func (pm *Manager) GetPaych(ctx context.Context, from, to address.Address, amt types.BigInt, reserve bool) (address.Address, cid.Cid, error) { +func (pm *Manager) GetPaych(ctx context.Context, from, to address.Address, amt types.BigInt, opts api.PaychGetOpts) (address.Address, cid.Cid, error) { + if !opts.Reserve && opts.OffChain { + return address.Undef, cid.Undef, xerrors.Errorf("can't fund payment channels without on-chain operations") + } + chanAccessor, err := pm.accessorByFromTo(from, to) if err != nil { return address.Undef, cid.Undef, err } - return chanAccessor.getPaych(ctx, amt, reserve) + return chanAccessor.getPaych(ctx, amt, opts) } func (pm *Manager) AvailableFunds(ctx context.Context, ch address.Address) (*api.ChannelAvailableFunds, error) { diff --git a/paychmgr/paychget_test.go b/paychmgr/paychget_test.go index 31207917721..f16b146f627 100644 --- a/paychmgr/paychget_test.go +++ b/paychmgr/paychget_test.go @@ -19,12 +19,30 @@ import ( init2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/init" tutils "github.com/filecoin-project/specs-actors/v2/support/testing" + "github.com/filecoin-project/lotus/api" lotusinit "github.com/filecoin-project/lotus/chain/actors/builtin/init" "github.com/filecoin-project/lotus/chain/actors/builtin/paych" paychmock "github.com/filecoin-project/lotus/chain/actors/builtin/paych/mock" "github.com/filecoin-project/lotus/chain/types" ) +var onChainReserve = api.PaychGetOpts{ + Reserve: true, + OffChain: false, +} +var onChainNoReserve = api.PaychGetOpts{ + Reserve: false, + OffChain: false, +} +var offChainReserve = api.PaychGetOpts{ + Reserve: true, + OffChain: true, +} +var offChainNoReserve = api.PaychGetOpts{ + Reserve: false, + OffChain: true, +} + func testChannelResponse(t *testing.T, ch address.Address) types.MessageReceipt { createChannelRet := init2.ExecReturn{ IDAddress: ch, @@ -55,7 +73,7 @@ func TestPaychGetCreateChannelMsg(t *testing.T) { require.NoError(t, err) amt := big.NewInt(10) - ch, mcid, err := mgr.GetPaych(ctx, from, to, amt, true) + ch, mcid, err := mgr.GetPaych(ctx, from, to, amt, onChainReserve) require.NoError(t, err) require.Equal(t, address.Undef, ch) @@ -65,6 +83,42 @@ func TestPaychGetCreateChannelMsg(t *testing.T) { require.Equal(t, amt, pushedMsg.Message.Value) } +func TestPaychGetOffchainNoReserveFails(t *testing.T) { + ctx := context.Background() + store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore())) + + from := tutils.NewIDAddr(t, 101) + to := tutils.NewIDAddr(t, 102) + + mock := newMockManagerAPI() + defer mock.close() + + mgr, err := newManager(store, mock) + require.NoError(t, err) + + amt := big.NewInt(10) + _, _, err = mgr.GetPaych(ctx, from, to, amt, offChainNoReserve) + require.Error(t, err) +} + +func TestPaychGetCreateOffchainReserveFails(t *testing.T) { + ctx := context.Background() + store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore())) + + from := tutils.NewIDAddr(t, 101) + to := tutils.NewIDAddr(t, 102) + + mock := newMockManagerAPI() + defer mock.close() + + mgr, err := newManager(store, mock) + require.NoError(t, err) + + amt := big.NewInt(10) + _, _, err = mgr.GetPaych(ctx, from, to, amt, offChainReserve) + require.Error(t, err) +} + // TestPaychGetCreateChannelThenAddFunds tests creating a channel and then // adding funds to it func TestPaychGetCreateChannelThenAddFunds(t *testing.T) { @@ -83,7 +137,7 @@ func TestPaychGetCreateChannelThenAddFunds(t *testing.T) { // Send create message for a channel with value 10 amt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, true) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, onChainReserve) require.NoError(t, err) // Should have no channels yet (message sent but channel not created) @@ -100,7 +154,7 @@ func TestPaychGetCreateChannelThenAddFunds(t *testing.T) { // 2. Request add funds - should block until create channel has completed amt2 := big.NewInt(5) - ch2, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, amt2, true) + ch2, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, amt2, onChainReserve) // 4. This GetPaych should return after create channel from first // GetPaych completes @@ -170,7 +224,7 @@ func TestPaychGetCreatePrefundedChannelThenAddFunds(t *testing.T) { // Send create message for a channel with value 10 amt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, false) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, onChainNoReserve) require.NoError(t, err) // Should have no channels yet (message sent but channel not created) @@ -187,7 +241,7 @@ func TestPaychGetCreatePrefundedChannelThenAddFunds(t *testing.T) { // 2. Request add funds - shouldn't block amt2 := big.NewInt(3) - ch2, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, amt2, true) + ch2, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, amt2, offChainReserve) // 4. This GetPaych should return after create channel from first // GetPaych completes @@ -240,7 +294,7 @@ func TestPaychGetCreateChannelWithErrorThenCreateAgain(t *testing.T) { // Send create message for a channel amt := big.NewInt(10) - _, mcid1, err := mgr.GetPaych(ctx, from, to, amt, true) + _, mcid1, err := mgr.GetPaych(ctx, from, to, amt, onChainReserve) require.NoError(t, err) // 1. Set up create channel response (sent in response to WaitForMsg()) @@ -258,7 +312,7 @@ func TestPaychGetCreateChannelWithErrorThenCreateAgain(t *testing.T) { // Because first channel create fails, this request // should be for channel create again. amt2 := big.NewInt(5) - ch2, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, true) + ch2, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, onChainReserve) require.NoError(t, err) require.Equal(t, address.Undef, ch2) @@ -305,7 +359,7 @@ func TestPaychGetRecoverAfterError(t *testing.T) { // Send create message for a channel amt := big.NewInt(10) - _, mcid, err := mgr.GetPaych(ctx, from, to, amt, true) + _, mcid, err := mgr.GetPaych(ctx, from, to, amt, onChainReserve) require.NoError(t, err) // Send error create channel response @@ -316,7 +370,7 @@ func TestPaychGetRecoverAfterError(t *testing.T) { // Send create message for a channel again amt2 := big.NewInt(7) - _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, true) + _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, onChainReserve) require.NoError(t, err) // Send success create channel response @@ -357,7 +411,7 @@ func TestPaychGetRecoverAfterAddFundsError(t *testing.T) { // Send create message for a channel amt := big.NewInt(10) - _, mcid1, err := mgr.GetPaych(ctx, from, to, amt, true) + _, mcid1, err := mgr.GetPaych(ctx, from, to, amt, onChainReserve) require.NoError(t, err) // Send success create channel response @@ -366,7 +420,7 @@ func TestPaychGetRecoverAfterAddFundsError(t *testing.T) { // Send add funds message for channel amt2 := big.NewInt(5) - _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, true) + _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, onChainReserve) require.NoError(t, err) // Send error add funds response @@ -393,7 +447,7 @@ func TestPaychGetRecoverAfterAddFundsError(t *testing.T) { // Send add funds message for channel again amt3 := big.NewInt(2) - _, mcid3, err := mgr.GetPaych(ctx, from, to, amt3, true) + _, mcid3, err := mgr.GetPaych(ctx, from, to, amt3, onChainReserve) require.NoError(t, err) // Send success add funds response @@ -438,7 +492,7 @@ func TestPaychGetRestartAfterCreateChannelMsg(t *testing.T) { // Send create message for a channel with value 10 amt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, true) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, onChainReserve) require.NoError(t, err) // Simulate shutting down system @@ -465,7 +519,7 @@ func TestPaychGetRestartAfterCreateChannelMsg(t *testing.T) { // 2. Request add funds - should block until create channel has completed amt2 := big.NewInt(5) - ch2, addFundsMsgCid, err := mgr2.GetPaych(ctx, from, to, amt2, true) + ch2, addFundsMsgCid, err := mgr2.GetPaych(ctx, from, to, amt2, onChainReserve) // 4. This GetPaych should return after create channel from first // GetPaych completes @@ -517,7 +571,7 @@ func TestPaychGetRestartAfterAddFundsMsg(t *testing.T) { // Send create message for a channel amt := big.NewInt(10) - _, mcid1, err := mgr.GetPaych(ctx, from, to, amt, true) + _, mcid1, err := mgr.GetPaych(ctx, from, to, amt, onChainReserve) require.NoError(t, err) // Send success create channel response @@ -526,7 +580,7 @@ func TestPaychGetRestartAfterAddFundsMsg(t *testing.T) { // Send add funds message for channel amt2 := big.NewInt(5) - _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, true) + _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, onChainReserve) require.NoError(t, err) // Simulate shutting down system @@ -580,7 +634,7 @@ func TestPaychGetWait(t *testing.T) { // 1. Get amt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, true) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, onChainReserve) require.NoError(t, err) expch := tutils.NewIDAddr(t, 100) @@ -603,7 +657,7 @@ func TestPaychGetWait(t *testing.T) { // Request add funds amt2 := big.NewInt(15) - _, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, amt2, true) + _, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, amt2, onChainReserve) require.NoError(t, err) go func() { @@ -637,7 +691,7 @@ func TestPaychGetWaitErr(t *testing.T) { // 1. Create channel amt := big.NewInt(10) - _, mcid, err := mgr.GetPaych(ctx, from, to, amt, true) + _, mcid, err := mgr.GetPaych(ctx, from, to, amt, onChainReserve) require.NoError(t, err) done := make(chan address.Address) @@ -683,7 +737,7 @@ func TestPaychGetWaitCtx(t *testing.T) { require.NoError(t, err) amt := big.NewInt(10) - _, mcid, err := mgr.GetPaych(ctx, from, to, amt, true) + _, mcid, err := mgr.GetPaych(ctx, from, to, amt, onChainReserve) require.NoError(t, err) // When the context is cancelled, should unblock wait @@ -714,7 +768,7 @@ func TestPaychGetMergeAddFunds(t *testing.T) { // Send create message for a channel with value 10 createAmt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, true) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, onChainReserve) require.NoError(t, err) // Queue up two add funds requests behind create channel @@ -732,7 +786,7 @@ func TestPaychGetMergeAddFunds(t *testing.T) { // Request add funds - should block until create channel has completed var err error - addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1, true) + addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1, onChainReserve) require.NoError(t, err) }() @@ -741,7 +795,7 @@ func TestPaychGetMergeAddFunds(t *testing.T) { // Request add funds again - should merge with waiting add funds request var err error - addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, true) + addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, onChainReserve) require.NoError(t, err) }() // Wait for add funds requests to be queued up @@ -810,7 +864,7 @@ func TestPaychGetMergePrefundAndReserve(t *testing.T) { // Send create message for a channel with value 10 createAmt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, true) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, onChainReserve) require.NoError(t, err) // Queue up two add funds requests behind create channel @@ -828,7 +882,7 @@ func TestPaychGetMergePrefundAndReserve(t *testing.T) { // Request add funds - should block until create channel has completed var err error - addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1, false) + addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1, onChainNoReserve) require.NoError(t, err) }() @@ -837,7 +891,7 @@ func TestPaychGetMergePrefundAndReserve(t *testing.T) { // Request add funds again - should merge with waiting add funds request var err error - addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, true) + addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, onChainReserve) require.NoError(t, err) }() // Wait for add funds requests to be queued up @@ -906,7 +960,7 @@ func TestPaychGetMergePrefundAndReservePrefunded(t *testing.T) { // Send create message for a channel with value 10 createAmt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, false) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, onChainNoReserve) require.NoError(t, err) // Queue up two add funds requests behind create channel @@ -924,7 +978,7 @@ func TestPaychGetMergePrefundAndReservePrefunded(t *testing.T) { // Request add funds - should block until create channel has completed var err error - addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1, false) + addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1, onChainNoReserve) require.NoError(t, err) }() @@ -933,7 +987,7 @@ func TestPaychGetMergePrefundAndReservePrefunded(t *testing.T) { // Request add funds again - should merge with waiting add funds request var err error - addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, true) + addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, onChainReserve) require.NoError(t, err) }() // Wait for add funds requests to be queued up @@ -987,6 +1041,247 @@ func TestPaychGetMergePrefundAndReservePrefunded(t *testing.T) { require.Equal(t, addFundsAmt1, addFundsMsg.Message.Value) } +func TestPaychGetMergePrefundAndReservePrefundedOneOffchain(t *testing.T) { + ctx := context.Background() + store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore())) + + ch := tutils.NewIDAddr(t, 100) + from := tutils.NewIDAddr(t, 101) + to := tutils.NewIDAddr(t, 102) + + mock := newMockManagerAPI() + defer mock.close() + + mgr, err := newManager(store, mock) + require.NoError(t, err) + + // Send create message for a channel with value 10 + createAmt := big.NewInt(10) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, onChainNoReserve) + require.NoError(t, err) + + // Queue up two add funds requests behind create channel + var addFundsSent sync.WaitGroup + addFundsSent.Add(2) + + addFundsAmt1 := big.NewInt(5) // 1 reserves + addFundsAmt2 := big.NewInt(3) // 2 reserves + var addFundsCh1 address.Address + var addFundsCh2 address.Address + var addFundsMcid1 cid.Cid + var addFundsMcid2 cid.Cid + go func() { + defer addFundsSent.Done() + + // Request add funds - should block until create channel has completed + var err error + addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1, offChainReserve) + require.NoError(t, err) + }() + + go func() { + defer addFundsSent.Done() + + // Request add funds again - should merge with waiting add funds request + var err error + addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, onChainReserve) + require.NoError(t, err) + }() + // Wait for add funds requests to be queued up + waitForQueueSize(t, mgr, from, to, 2) + + // Send create channel response + response := testChannelResponse(t, ch) + mock.receiveMsgResponse(createMsgCid, response) + + // Wait for create channel response + chres, err := mgr.GetPaychWaitReady(ctx, createMsgCid) + require.NoError(t, err) + require.Equal(t, ch, chres) + + // Wait for add funds requests to be sent + addFundsSent.Wait() + + // Expect add funds requests to have same channel as create channel and + // same message cid as each other (because they should have been merged) + require.Equal(t, ch, addFundsCh1) + require.Equal(t, ch, addFundsCh2) + require.Equal(t, cid.Undef, addFundsMcid1) + require.Equal(t, cid.Undef, addFundsMcid2) + + // Make sure that one create channel message was sent + require.Equal(t, 1, mock.pushedMessageCount()) + + // Check create message amount is correct + createMsg := mock.pushedMessages(createMsgCid) + require.Equal(t, from, createMsg.Message.From) + require.Equal(t, lotusinit.Address, createMsg.Message.To) + require.Equal(t, createAmt, createMsg.Message.Value) +} + +func TestPaychGetMergePrefundAndReservePrefundedBothOffchainOneFail(t *testing.T) { + ctx := context.Background() + store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore())) + + ch := tutils.NewIDAddr(t, 100) + from := tutils.NewIDAddr(t, 101) + to := tutils.NewIDAddr(t, 102) + + mock := newMockManagerAPI() + defer mock.close() + + mgr, err := newManager(store, mock) + require.NoError(t, err) + + // Send create message for a channel with value 10 + createAmt := big.NewInt(10) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, onChainNoReserve) + require.NoError(t, err) + + // Queue up two add funds requests behind create channel + var addFundsSent sync.WaitGroup + addFundsSent.Add(2) + + addFundsAmt1 := big.NewInt(5) // 1 reserves + addFundsAmt2 := big.NewInt(6) // 2 reserves too much + var addFundsCh1 address.Address + var addFundsCh2 address.Address + var addFundsMcid1 cid.Cid + var addFundsMcid2 cid.Cid + go func() { + defer addFundsSent.Done() + + // Request add funds - should block until create channel has completed + var err error + addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1, offChainReserve) + require.NoError(t, err) + }() + + go func() { + defer addFundsSent.Done() + + // Request add funds again - should merge with waiting add funds request + var err error + addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, offChainReserve) + require.Error(t, err) + }() + // Wait for add funds requests to be queued up + waitForQueueSize(t, mgr, from, to, 2) + + // Send create channel response + response := testChannelResponse(t, ch) + mock.receiveMsgResponse(createMsgCid, response) + + // Wait for create channel response + chres, err := mgr.GetPaychWaitReady(ctx, createMsgCid) + require.NoError(t, err) + require.Equal(t, ch, chres) + + // Wait for add funds requests to be sent + addFundsSent.Wait() + + // Expect add funds requests to have same channel as create channel and + // same message cid as each other (because they should have been merged) + require.Equal(t, ch, addFundsCh1) + require.Equal(t, ch, addFundsCh2) + require.Equal(t, cid.Undef, addFundsMcid1) + require.Equal(t, cid.Undef, addFundsMcid2) + + // Make sure that one create channel message was sent + require.Equal(t, 1, mock.pushedMessageCount()) + + // Check create message amount is correct + createMsg := mock.pushedMessages(createMsgCid) + require.Equal(t, from, createMsg.Message.From) + require.Equal(t, lotusinit.Address, createMsg.Message.To) + require.Equal(t, createAmt, createMsg.Message.Value) +} + +func TestPaychGetMergePrefundAndReserveOneOffchainOneFail(t *testing.T) { + ctx := context.Background() + store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore())) + + ch := tutils.NewIDAddr(t, 100) + from := tutils.NewIDAddr(t, 101) + to := tutils.NewIDAddr(t, 102) + + mock := newMockManagerAPI() + defer mock.close() + + mgr, err := newManager(store, mock) + require.NoError(t, err) + + // Send create message for a channel with value 10 + createAmt := big.NewInt(10) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, onChainReserve) + require.NoError(t, err) + + // Queue up two add funds requests behind create channel + var addFundsSent sync.WaitGroup + addFundsSent.Add(2) + + addFundsAmt1 := big.NewInt(5) // 1 reserves + addFundsAmt2 := big.NewInt(6) // 2 reserves + var addFundsCh1 address.Address + var addFundsCh2 address.Address + var addFundsMcid1 cid.Cid + var addFundsMcid2 cid.Cid + go func() { + defer addFundsSent.Done() + + // Request add funds - should block until create channel has completed + var err error + addFundsCh1, addFundsMcid1, err = mgr.GetPaych(ctx, from, to, addFundsAmt1, onChainReserve) + require.NoError(t, err) + }() + + go func() { + defer addFundsSent.Done() + + // Request add funds again - should merge with waiting add funds request + var err error + addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, offChainReserve) + require.Error(t, err) + }() + // Wait for add funds requests to be queued up + waitForQueueSize(t, mgr, from, to, 2) + + // Send create channel response + response := testChannelResponse(t, ch) + mock.receiveMsgResponse(createMsgCid, response) + + // Wait for create channel response + chres, err := mgr.GetPaychWaitReady(ctx, createMsgCid) + require.NoError(t, err) + require.Equal(t, ch, chres) + + // Wait for add funds requests to be sent + addFundsSent.Wait() + + // Expect add funds requests to have same channel as create channel and + // same message cid as each other (because they should have been merged) + require.Equal(t, ch, addFundsCh1) + require.Equal(t, ch, addFundsCh2) + require.NotEqual(t, cid.Undef, addFundsMcid1) + require.Equal(t, cid.Undef, addFundsMcid2) + + // Make sure that one create channel message was sent + require.Equal(t, 2, mock.pushedMessageCount()) + + // Check create message amount is correct + createMsg := mock.pushedMessages(createMsgCid) + require.Equal(t, from, createMsg.Message.From) + require.Equal(t, lotusinit.Address, createMsg.Message.To) + require.Equal(t, createAmt, createMsg.Message.Value) + + // Check merged add funds amount is the sum of the individual + // amounts + addFundsMsg := mock.pushedMessages(addFundsMcid1) + require.Equal(t, from, addFundsMsg.Message.From) + require.Equal(t, ch, addFundsMsg.Message.To) + require.Equal(t, addFundsAmt1, addFundsMsg.Message.Value) +} + // TestPaychGetMergeAddFundsCtxCancelOne tests that when a queued add funds // request is cancelled, its amount is removed from the total merged add funds func TestPaychGetMergeAddFundsCtxCancelOne(t *testing.T) { @@ -1005,7 +1300,7 @@ func TestPaychGetMergeAddFundsCtxCancelOne(t *testing.T) { // Send create message for a channel with value 10 createAmt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, true) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, onChainReserve) require.NoError(t, err) // Queue up two add funds requests behind create channel @@ -1022,7 +1317,7 @@ func TestPaychGetMergeAddFundsCtxCancelOne(t *testing.T) { defer addFundsSent.Done() // Request add funds - should block until create channel has completed - _, _, addFundsErr1 = mgr.GetPaych(addFundsCtx1, from, to, addFundsAmt1, true) + _, _, addFundsErr1 = mgr.GetPaych(addFundsCtx1, from, to, addFundsAmt1, onChainReserve) }() go func() { @@ -1030,7 +1325,7 @@ func TestPaychGetMergeAddFundsCtxCancelOne(t *testing.T) { // Request add funds again - should merge with waiting add funds request var err error - addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, true) + addFundsCh2, addFundsMcid2, err = mgr.GetPaych(ctx, from, to, addFundsAmt2, onChainReserve) require.NoError(t, err) }() // Wait for add funds requests to be queued up @@ -1102,7 +1397,7 @@ func TestPaychGetMergeAddFundsCtxCancelAll(t *testing.T) { // Send create message for a channel with value 10 createAmt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, true) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, onChainReserve) require.NoError(t, err) // Queue up two add funds requests behind create channel @@ -1117,14 +1412,14 @@ func TestPaychGetMergeAddFundsCtxCancelAll(t *testing.T) { defer addFundsSent.Done() // Request add funds - should block until create channel has completed - _, _, addFundsErr1 = mgr.GetPaych(addFundsCtx1, from, to, big.NewInt(5), true) + _, _, addFundsErr1 = mgr.GetPaych(addFundsCtx1, from, to, big.NewInt(5), onChainReserve) }() go func() { defer addFundsSent.Done() // Request add funds again - should merge with waiting add funds request - _, _, addFundsErr2 = mgr.GetPaych(addFundsCtx2, from, to, big.NewInt(3), true) + _, _, addFundsErr2 = mgr.GetPaych(addFundsCtx2, from, to, big.NewInt(3), onChainReserve) }() // Wait for add funds requests to be queued up waitForQueueSize(t, mgr, from, to, 2) @@ -1189,7 +1484,7 @@ func TestPaychAvailableFunds(t *testing.T) { // Send create message for a channel with value 10 createAmt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, true) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, onChainReserve) require.NoError(t, err) // Available funds should reflect create channel message sent @@ -1214,7 +1509,7 @@ func TestPaychAvailableFunds(t *testing.T) { // Request add funds - should block until create channel has completed var err error - _, addFundsMcid, err = mgr.GetPaych(ctx, from, to, addFundsAmt, true) + _, addFundsMcid, err = mgr.GetPaych(ctx, from, to, addFundsAmt, onChainReserve) require.NoError(t, err) }() diff --git a/paychmgr/paychvoucherfunds_test.go b/paychmgr/paychvoucherfunds_test.go index d13a5815eb4..4a2f7e31a83 100644 --- a/paychmgr/paychvoucherfunds_test.go +++ b/paychmgr/paychvoucherfunds_test.go @@ -46,7 +46,7 @@ func TestPaychAddVoucherAfterAddFunds(t *testing.T) { // Send create message for a channel with value 10 createAmt := big.NewInt(10) - _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, true) + _, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, onChainReserve) require.NoError(t, err) // Send create channel response @@ -82,7 +82,7 @@ func TestPaychAddVoucherAfterAddFunds(t *testing.T) { require.Equal(t, res.Shortfall, excessAmt) // Add funds so as to cover the voucher shortfall - _, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, excessAmt, true) + _, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, excessAmt, onChainReserve) require.NoError(t, err) // Trigger add funds confirmation diff --git a/paychmgr/settle_test.go b/paychmgr/settle_test.go index e674d63f4e0..bc88df2f0d6 100644 --- a/paychmgr/settle_test.go +++ b/paychmgr/settle_test.go @@ -29,7 +29,7 @@ func TestPaychSettle(t *testing.T) { require.NoError(t, err) amt := big.NewInt(10) - _, mcid, err := mgr.GetPaych(ctx, from, to, amt, true) + _, mcid, err := mgr.GetPaych(ctx, from, to, amt, onChainReserve) require.NoError(t, err) // Send channel create response @@ -49,7 +49,7 @@ func TestPaychSettle(t *testing.T) { // (should create a new channel because the previous channel // is settling) amt2 := big.NewInt(5) - _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, true) + _, mcid2, err := mgr.GetPaych(ctx, from, to, amt2, onChainReserve) require.NoError(t, err) require.NotEqual(t, cid.Undef, mcid2) diff --git a/paychmgr/simple.go b/paychmgr/simple.go index def24d1cf5c..ffb13b6c233 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -33,20 +33,20 @@ type fundsReq struct { ctx context.Context promise chan *paychFundsRes amt types.BigInt - reserve bool + opts api.PaychGetOpts lk sync.Mutex // merge parent, if this req is part of a merge merge *mergedFundsReq } -func newFundsReq(ctx context.Context, amt types.BigInt, reserve bool) *fundsReq { +func newFundsReq(ctx context.Context, amt types.BigInt, opts api.PaychGetOpts) *fundsReq { promise := make(chan *paychFundsRes, 1) return &fundsReq{ ctx: ctx, promise: promise, amt: amt, - reserve: reserve, + opts: opts, } } @@ -108,8 +108,12 @@ func newMergedFundsReq(reqs []*fundsReq) *mergedFundsReq { } sort.Slice(m.reqs, func(i, j int) bool { - if m.reqs[i].reserve != m.reqs[j].reserve { // non-reserve first - return m.reqs[i].reserve + if m.reqs[i].opts.OffChain != m.reqs[j].opts.OffChain { // off-chain first + return m.reqs[i].opts.OffChain + } + + if m.reqs[i].opts.Reserve != m.reqs[j].opts.Reserve { // non-reserve after off-chain + return m.reqs[i].opts.Reserve } // sort by amount asc (reducing latency for smaller requests) @@ -154,7 +158,7 @@ func (m *mergedFundsReq) sum() (types.BigInt, types.BigInt) { for _, r := range m.reqs { if r.isActive() { sum = types.BigAdd(sum, r.amt) - if !r.reserve { + if !r.opts.Reserve { avail = types.BigAdd(avail, r.amt) } } @@ -164,17 +168,30 @@ func (m *mergedFundsReq) sum() (types.BigInt, types.BigInt) { } // completeAmount completes first non-reserving requests up to the available amount -func (m *mergedFundsReq) completeAmount(avail types.BigInt, channelInfo *ChannelInfo) (*paychFundsRes, types.BigInt) { - used := types.NewInt(0) +func (m *mergedFundsReq) completeAmount(avail types.BigInt, channelInfo *ChannelInfo) (*paychFundsRes, types.BigInt, types.BigInt) { + used, failed := types.NewInt(0), types.NewInt(0) next := 0 + + // order: [offchain+reserve, !offchain+reserve, !offchain+!reserve] for i, r := range m.reqs { - if !r.reserve { + if !r.opts.Reserve { // non-reserving request are put after reserving requests, so we are done here break } if r.amt.GreaterThan(types.BigSub(avail, used)) { // requests are sorted by amount ascending, so if we hit this, there aren't any more requests we can fill + + if r.opts.OffChain { + // can't fill, so OffChain want an error + if r.isActive() { + failed = types.BigAdd(failed, r.amt) + r.onComplete(&paychFundsRes{channel: *channelInfo.Channel, err: xerrors.Errorf("not enough available funds in the payment channel")}) + } + next = i + 1 + continue + } + break } @@ -190,9 +207,34 @@ func (m *mergedFundsReq) completeAmount(avail types.BigInt, channelInfo *Channel m.reqs = m.reqs[next:] if len(m.reqs) == 0 { - return &paychFundsRes{channel: *channelInfo.Channel}, used + return &paychFundsRes{channel: *channelInfo.Channel}, used, failed } - return nil, used + return nil, used, failed +} + +func (m *mergedFundsReq) failOffChain(msg string) (*paychFundsRes, types.BigInt) { + next := 0 + freed := types.NewInt(0) + + for i, r := range m.reqs { + if !r.opts.OffChain { + break + } + + freed = types.BigAdd(freed, r.amt) + if !r.isActive() { + continue + } + r.onComplete(&paychFundsRes{err: xerrors.New(msg)}) + next = i + 1 + } + + m.reqs = m.reqs[next:] + if len(m.reqs) == 0 { + return &paychFundsRes{err: xerrors.New(msg)}, freed + } + + return nil, freed } // getPaych ensures that a channel exists between the from and to addresses, @@ -206,9 +248,9 @@ func (m *mergedFundsReq) completeAmount(avail types.BigInt, channelInfo *Channel // address and the CID of the new add funds message. // If an operation returns an error, subsequent waiting operations will still // be attempted. -func (ca *channelAccessor) getPaych(ctx context.Context, amt types.BigInt, reserve bool) (address.Address, cid.Cid, error) { +func (ca *channelAccessor) getPaych(ctx context.Context, amt types.BigInt, opts api.PaychGetOpts) (address.Address, cid.Cid, error) { // Add the request to add funds to a queue and wait for the result - freq := newFundsReq(ctx, amt, reserve) + freq := newFundsReq(ctx, amt, opts) ca.enqueue(ctx, freq) select { case res := <-freq.promise: @@ -398,6 +440,12 @@ func (ca *channelAccessor) processTask(merged *mergedFundsReq, amt, avail types. // If a channel has not yet been created, create one. if channelInfo == nil { + res, freed := merged.failOffChain("payment channel doesn't exist") + if res != nil { + return res + } + amt = types.BigSub(amt, freed) + mcid, err := ca.createPaych(ctx, amt, avail) if err != nil { return &paychFundsRes{err: err} @@ -536,14 +584,14 @@ func (ca *channelAccessor) completeAvailable(ctx context.Context, merged *merged ci.AvailableAmount = big.Sub(ci.AvailableAmount, avail) }) - res, used := merged.completeAmount(avail, channelInfo) + res, used, failed := merged.completeAmount(avail, channelInfo) // return any unused reserved funds (e.g. from cancelled requests) ca.mutateChannelInfo(ctx, channelInfo.ChannelID, func(ci *ChannelInfo) { ci.AvailableAmount = types.BigAdd(ci.AvailableAmount, types.BigSub(avail, used)) }) - return res, types.BigSub(amt, used) + return res, types.BigSub(amt, types.BigAdd(used, failed)) } // addFunds sends a message to add funds to the channel and returns the message cid diff --git a/testplans/lotus-soup/deals_e2e.go b/testplans/lotus-soup/deals_e2e.go index bc970350837..d9be97f6cae 100644 --- a/testplans/lotus-soup/deals_e2e.go +++ b/testplans/lotus-soup/deals_e2e.go @@ -207,7 +207,10 @@ func initPaymentChannel(t *testkit.TestEnvironment, ctx context.Context, cl *tes t.RecordMessage("my balance: %d", balance) t.RecordMessage("creating payment channel; from=%s, to=%s, funds=%d", cl.Wallet.Address, recv.WalletAddr, balance) - channel, err := cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, balance, true) + channel, err := cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, balance, api.PaychGetOpts{ + Reserve: true, + OffChain: false, + }) if err != nil { return fmt.Errorf("failed to create payment channel: %w", err) } @@ -230,7 +233,10 @@ func initPaymentChannel(t *testkit.TestEnvironment, ctx context.Context, cl *tes // we wait for 2 confirmations, so we have the assurance the channel is tracked. t.RecordMessage("reloading paych; now it should have an address") - channel, err = cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, big.Zero(), true) + channel, err = cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, big.Zero(), api.PaychGetOpts{ + Reserve: true, + OffChain: false, + }) if err != nil { return fmt.Errorf("failed to reload payment channel: %w", err) } diff --git a/testplans/lotus-soup/paych/stress.go b/testplans/lotus-soup/paych/stress.go index e0d324f03e9..2f90308d8ee 100644 --- a/testplans/lotus-soup/paych/stress.go +++ b/testplans/lotus-soup/paych/stress.go @@ -124,7 +124,10 @@ func runSender(ctx context.Context, t *testkit.TestEnvironment, clients []*testk time.Sleep(20 * time.Second) - channel, err := cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, channelAmt, true) + channel, err := cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, channelAmt, api.PaychGetOpts{ + Reserve: true, + OffChain: false, + }) if err != nil { return fmt.Errorf("failed to create payment channel: %w", err) } From 4235a97cf4f7f485c02b090cee9f5b70cdcdf77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Thu, 6 Jan 2022 16:26:25 +0100 Subject: [PATCH 13/24] retrieval: OffChainRetrieval config --- markets/retrievaladapter/client.go | 13 ++++++--- node/builder_chain.go | 4 ++- node/config/doc_gen.go | 8 ++++++ node/config/types.go | 5 ++++ node/modules/client.go | 42 ++++++++++++++++-------------- 5 files changed, 48 insertions(+), 24 deletions(-) diff --git a/markets/retrievaladapter/client.go b/markets/retrievaladapter/client.go index 601f9f2550f..60f41ca296d 100644 --- a/markets/retrievaladapter/client.go +++ b/markets/retrievaladapter/client.go @@ -41,6 +41,8 @@ func extractPaychReusedCid(c cid.Cid) (address.Address, error) { } type retrievalClientNode struct { + forceOffChain bool + chainAPI full.ChainAPI payAPI payapi.PaychAPI stateAPI full.StateAPI @@ -48,8 +50,13 @@ type retrievalClientNode struct { // NewRetrievalClientNode returns a new node adapter for a retrieval client that talks to the // Lotus Node -func NewRetrievalClientNode(payAPI payapi.PaychAPI, chainAPI full.ChainAPI, stateAPI full.StateAPI) retrievalmarket.RetrievalClientNode { - return &retrievalClientNode{payAPI: payAPI, chainAPI: chainAPI, stateAPI: stateAPI} +func NewRetrievalClientNode(forceOffChain bool, payAPI payapi.PaychAPI, chainAPI full.ChainAPI, stateAPI full.StateAPI) retrievalmarket.RetrievalClientNode { + return &retrievalClientNode{ + forceOffChain: forceOffChain, + chainAPI: chainAPI, + payAPI: payAPI, + stateAPI: stateAPI, + } } // GetOrCreatePaymentChannel sets up a new payment channel if one does not exist @@ -60,7 +67,7 @@ func (rcn *retrievalClientNode) GetOrCreatePaymentChannel(ctx context.Context, c // querying the chain ci, err := rcn.payAPI.PaychGet(ctx, clientAddress, minerAddress, clientFundsAvailable, api.PaychGetOpts{ Reserve: true, - OffChain: false, + OffChain: rcn.forceOffChain, }) if err != nil { return address.Undef, cid.Undef, err diff --git a/node/builder_chain.go b/node/builder_chain.go index 11283ec3a09..0d10dcb9b22 100644 --- a/node/builder_chain.go +++ b/node/builder_chain.go @@ -121,7 +121,7 @@ var ChainNode = Options( // Markets (retrieval) Override(new(discovery.PeerResolver), modules.RetrievalResolver), Override(new(retrievalmarket.BlockstoreAccessor), modules.RetrievalBlockstoreAccessor), - Override(new(retrievalmarket.RetrievalClient), modules.RetrievalClient), + Override(new(retrievalmarket.RetrievalClient), modules.RetrievalClient(false)), Override(new(dtypes.ClientDataTransfer), modules.NewClientGraphsyncDataTransfer), // Markets (storage) @@ -221,6 +221,8 @@ func ConfigFullNode(c interface{}) Option { ), Override(new(dtypes.Graphsync), modules.Graphsync(cfg.Client.SimultaneousTransfersForStorage, cfg.Client.SimultaneousTransfersForRetrieval)), + Override(new(retrievalmarket.RetrievalClient), modules.RetrievalClient(cfg.Client.OffChainRetrieval)), + If(cfg.Wallet.RemoteBackend != "", Override(new(*remotewallet.RemoteWallet), remotewallet.SetupRemoteWallet(cfg.Wallet.RemoteBackend)), ), diff --git a/node/config/doc_gen.go b/node/config/doc_gen.go index c3730cbace6..59181f9f625 100644 --- a/node/config/doc_gen.go +++ b/node/config/doc_gen.go @@ -105,6 +105,14 @@ and storage providers for storage deals`, Comment: `The maximum number of simultaneous data transfers between the client and storage providers for retrieval deals`, }, + { + Name: "OffChainRetrieval", + Type: "bool", + + Comment: `Require that retrievals perform no on-chain retrievals. Paid retrievals +without existing payment channels with available funds will fail instead +of automatically performing on-chain operations.`, + }, }, "Common": []DocField{ { diff --git a/node/config/types.go b/node/config/types.go index 715f4824861..7e9064614f5 100644 --- a/node/config/types.go +++ b/node/config/types.go @@ -383,6 +383,11 @@ type Client struct { // The maximum number of simultaneous data transfers between the client // and storage providers for retrieval deals SimultaneousTransfersForRetrieval uint64 + + // Require that retrievals perform no on-chain retrievals. Paid retrievals + // without existing payment channels with available funds will fail instead + // of automatically performing on-chain operations. + OffChainRetrieval bool } type Wallet struct { diff --git a/node/modules/client.go b/node/modules/client.go index 48f9dc3d778..1e74182041d 100644 --- a/node/modules/client.go +++ b/node/modules/client.go @@ -202,26 +202,28 @@ func StorageClient(lc fx.Lifecycle, h host.Host, dataTransfer dtypes.ClientDataT } // RetrievalClient creates a new retrieval client attached to the client blockstore -func RetrievalClient(lc fx.Lifecycle, h host.Host, r repo.LockedRepo, dt dtypes.ClientDataTransfer, payAPI payapi.PaychAPI, resolver discovery.PeerResolver, +func RetrievalClient(forceOffChain bool) func(lc fx.Lifecycle, h host.Host, r repo.LockedRepo, dt dtypes.ClientDataTransfer, payAPI payapi.PaychAPI, resolver discovery.PeerResolver, ds dtypes.MetadataDS, chainAPI full.ChainAPI, stateAPI full.StateAPI, accessor retrievalmarket.BlockstoreAccessor, j journal.Journal) (retrievalmarket.RetrievalClient, error) { - - adapter := retrievaladapter.NewRetrievalClientNode(payAPI, chainAPI, stateAPI) - network := rmnet.NewFromLibp2pHost(h) - ds = namespace.Wrap(ds, datastore.NewKey("/retrievals/client")) - client, err := retrievalimpl.NewClient(network, dt, adapter, resolver, ds, accessor) - if err != nil { - return nil, err + return func(lc fx.Lifecycle, h host.Host, r repo.LockedRepo, dt dtypes.ClientDataTransfer, payAPI payapi.PaychAPI, resolver discovery.PeerResolver, + ds dtypes.MetadataDS, chainAPI full.ChainAPI, stateAPI full.StateAPI, accessor retrievalmarket.BlockstoreAccessor, j journal.Journal) (retrievalmarket.RetrievalClient, error) { + adapter := retrievaladapter.NewRetrievalClientNode(forceOffChain, payAPI, chainAPI, stateAPI) + network := rmnet.NewFromLibp2pHost(h) + ds = namespace.Wrap(ds, datastore.NewKey("/retrievals/client")) + client, err := retrievalimpl.NewClient(network, dt, adapter, resolver, ds, accessor) + if err != nil { + return nil, err + } + client.OnReady(marketevents.ReadyLogger("retrieval client")) + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + client.SubscribeToEvents(marketevents.RetrievalClientLogger) + + evtType := j.RegisterEventType("markets/retrieval/client", "state_change") + client.SubscribeToEvents(markets.RetrievalClientJournaler(j, evtType)) + + return client.Start(ctx) + }, + }) + return client, nil } - client.OnReady(marketevents.ReadyLogger("retrieval client")) - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - client.SubscribeToEvents(marketevents.RetrievalClientLogger) - - evtType := j.RegisterEventType("markets/retrieval/client", "state_change") - client.SubscribeToEvents(markets.RetrievalClientJournaler(j, evtType)) - - return client.Start(ctx) - }, - }) - return client, nil } From 8f9e730ad66d7d3d973dc4204a6645cb09f31c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Thu, 6 Jan 2022 18:02:34 +0100 Subject: [PATCH 14/24] paych: Better off-chain errors --- markets/retrievaladapter/client.go | 1 + paychmgr/simple.go | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/markets/retrievaladapter/client.go b/markets/retrievaladapter/client.go index 60f41ca296d..97a157b19cd 100644 --- a/markets/retrievaladapter/client.go +++ b/markets/retrievaladapter/client.go @@ -70,6 +70,7 @@ func (rcn *retrievalClientNode) GetOrCreatePaymentChannel(ctx context.Context, c OffChain: rcn.forceOffChain, }) if err != nil { + log.Errorw("paych get failed", "error", err) return address.Undef, cid.Undef, err } if ci.WaitSentinel == cid.Undef { diff --git a/paychmgr/simple.go b/paychmgr/simple.go index ffb13b6c233..ef0f81b8748 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -186,7 +186,10 @@ func (m *mergedFundsReq) completeAmount(avail types.BigInt, channelInfo *Channel // can't fill, so OffChain want an error if r.isActive() { failed = types.BigAdd(failed, r.amt) - r.onComplete(&paychFundsRes{channel: *channelInfo.Channel, err: xerrors.Errorf("not enough available funds in the payment channel")}) + r.onComplete(&paychFundsRes{ + channel: *channelInfo.Channel, + err: xerrors.Errorf("not enough funds available in the payment channel %s; add funds with 'lotus paych add-funds %s %s %s'", channelInfo.Channel, channelInfo.from(), channelInfo.to(), types.FIL(r.amt).Unitless()), + }) } next = i + 1 continue @@ -212,7 +215,7 @@ func (m *mergedFundsReq) completeAmount(avail types.BigInt, channelInfo *Channel return nil, used, failed } -func (m *mergedFundsReq) failOffChain(msg string) (*paychFundsRes, types.BigInt) { +func (m *mergedFundsReq) failOffChainNoChannel(from, to address.Address) (*paychFundsRes, types.BigInt) { next := 0 freed := types.NewInt(0) @@ -225,13 +228,13 @@ func (m *mergedFundsReq) failOffChain(msg string) (*paychFundsRes, types.BigInt) if !r.isActive() { continue } - r.onComplete(&paychFundsRes{err: xerrors.New(msg)}) + r.onComplete(&paychFundsRes{err: xerrors.Errorf("payment channel doesn't exist, create with 'lotus paych add-funds %s %s %s'", from, to, types.FIL(r.amt).Unitless())}) next = i + 1 } m.reqs = m.reqs[next:] if len(m.reqs) == 0 { - return &paychFundsRes{err: xerrors.New(msg)}, freed + return &paychFundsRes{err: xerrors.Errorf("payment channel doesn't exist, create with 'lotus paych add-funds %s %s 0'", from, to)}, freed } return nil, freed @@ -440,7 +443,7 @@ func (ca *channelAccessor) processTask(merged *mergedFundsReq, amt, avail types. // If a channel has not yet been created, create one. if channelInfo == nil { - res, freed := merged.failOffChain("payment channel doesn't exist") + res, freed := merged.failOffChainNoChannel(ca.from, ca.to) if res != nil { return res } From 550e2743d727f5322ccfc7261d42c7509a2b185d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Thu, 6 Jan 2022 19:51:26 +0100 Subject: [PATCH 15/24] paych: Don't return settling/collected chennals from OutboundActiveByFromTo --- build/openrpc/full.json.gz | Bin 26594 -> 26976 bytes documentation/en/default-lotus-config.toml | 8 ++ paychmgr/paych.go | 2 +- paychmgr/paych_test.go | 12 +-- paychmgr/paychget_test.go | 108 ++++++++++++++++++++- paychmgr/simple.go | 2 +- paychmgr/store.go | 20 +++- 7 files changed, 140 insertions(+), 12 deletions(-) diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index ae8f0b86655688ef335478f8abf47bf58d1ca299..55e96a3aa48e38cb9febb44d3c03b0c3bbc5f34b 100644 GIT binary patch literal 26976 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 GVE+e$XyMKP diff --git a/documentation/en/default-lotus-config.toml b/documentation/en/default-lotus-config.toml index 3cb8977b290..016e1290cb9 100644 --- a/documentation/en/default-lotus-config.toml +++ b/documentation/en/default-lotus-config.toml @@ -121,6 +121,14 @@ # env var: LOTUS_CLIENT_SIMULTANEOUSTRANSFERSFORRETRIEVAL #SimultaneousTransfersForRetrieval = 20 + # Require that retrievals perform no on-chain retrievals. Paid retrievals + # without existing payment channels with available funds will fail instead + # of automatically performing on-chain operations. + # + # type: bool + # env var: LOTUS_CLIENT_OFFCHAINRETRIEVAL + #OffChainRetrieval = false + [Wallet] # type: string diff --git a/paychmgr/paych.go b/paychmgr/paych.go index 16c6604c6ca..5fdb4d8842e 100644 --- a/paychmgr/paych.go +++ b/paychmgr/paych.go @@ -106,7 +106,7 @@ func (ca *channelAccessor) outboundActiveByFromTo(ctx context.Context, from, to ca.lk.Lock() defer ca.lk.Unlock() - return ca.store.OutboundActiveByFromTo(ctx, from, to) + return ca.store.OutboundActiveByFromTo(ctx, ca.api, from, to) } // createVoucher creates a voucher with the given specification, setting its diff --git a/paychmgr/paych_test.go b/paychmgr/paych_test.go index 165c945b8cb..24214d1b585 100644 --- a/paychmgr/paych_test.go +++ b/paychmgr/paych_test.go @@ -465,18 +465,18 @@ func TestAddVoucherInboundWalletKey(t *testing.T) { toAcct := tutils.NewActorAddr(t, "toAct") // Create an actor for the channel in state - act := &types.Actor{ - Code: builtin.AccountActorCodeID, - Head: cid.Cid{}, - Nonce: 0, - Balance: types.NewInt(20), - } mock := newMockManagerAPI() mock.setAccountAddress(fromAcct, from) mock.setAccountAddress(toAcct, to) + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } mock.setPaychState(ch, act, paychmock.NewMockPayChState(fromAcct, toAcct, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) // Create a manager diff --git a/paychmgr/paychget_test.go b/paychmgr/paychget_test.go index f16b146f627..c53a85d86a0 100644 --- a/paychmgr/paychget_test.go +++ b/paychmgr/paychget_test.go @@ -132,6 +132,14 @@ func TestPaychGetCreateChannelThenAddFunds(t *testing.T) { mock := newMockManagerAPI() defer mock.close() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr, err := newManager(store, mock) require.NoError(t, err) @@ -219,6 +227,14 @@ func TestPaychGetCreatePrefundedChannelThenAddFunds(t *testing.T) { mock := newMockManagerAPI() defer mock.close() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr, err := newManager(store, mock) require.NoError(t, err) @@ -406,6 +422,14 @@ func TestPaychGetRecoverAfterAddFundsError(t *testing.T) { mock := newMockManagerAPI() defer mock.close() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr, err := newManager(store, mock) require.NoError(t, err) @@ -502,6 +526,14 @@ func TestPaychGetRestartAfterCreateChannelMsg(t *testing.T) { mock2 := newMockManagerAPI() defer mock2.close() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock2.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr2, err := newManager(store, mock2) require.NoError(t, err) @@ -566,6 +598,14 @@ func TestPaychGetRestartAfterAddFundsMsg(t *testing.T) { mock := newMockManagerAPI() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr, err := newManager(store, mock) require.NoError(t, err) @@ -590,6 +630,8 @@ func TestPaychGetRestartAfterAddFundsMsg(t *testing.T) { mock2 := newMockManagerAPI() defer mock2.close() + mock2.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr2, err := newManager(store, mock2) require.NoError(t, err) @@ -625,10 +667,19 @@ func TestPaychGetWait(t *testing.T) { from := tutils.NewIDAddr(t, 101) to := tutils.NewIDAddr(t, 102) + expch := tutils.NewIDAddr(t, 100) mock := newMockManagerAPI() defer mock.close() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock.setPaychState(expch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr, err := newManager(store, mock) require.NoError(t, err) @@ -637,7 +688,6 @@ func TestPaychGetWait(t *testing.T) { _, createMsgCid, err := mgr.GetPaych(ctx, from, to, amt, onChainReserve) require.NoError(t, err) - expch := tutils.NewIDAddr(t, 100) go func() { // 3. Send response response := testChannelResponse(t, expch) @@ -763,6 +813,14 @@ func TestPaychGetMergeAddFunds(t *testing.T) { mock := newMockManagerAPI() defer mock.close() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr, err := newManager(store, mock) require.NoError(t, err) @@ -859,6 +917,14 @@ func TestPaychGetMergePrefundAndReserve(t *testing.T) { mock := newMockManagerAPI() defer mock.close() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr, err := newManager(store, mock) require.NoError(t, err) @@ -955,6 +1021,14 @@ func TestPaychGetMergePrefundAndReservePrefunded(t *testing.T) { mock := newMockManagerAPI() defer mock.close() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr, err := newManager(store, mock) require.NoError(t, err) @@ -1052,6 +1126,14 @@ func TestPaychGetMergePrefundAndReservePrefundedOneOffchain(t *testing.T) { mock := newMockManagerAPI() defer mock.close() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr, err := newManager(store, mock) require.NoError(t, err) @@ -1130,6 +1212,14 @@ func TestPaychGetMergePrefundAndReservePrefundedBothOffchainOneFail(t *testing.T mock := newMockManagerAPI() defer mock.close() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr, err := newManager(store, mock) require.NoError(t, err) @@ -1208,6 +1298,14 @@ func TestPaychGetMergePrefundAndReserveOneOffchainOneFail(t *testing.T) { mock := newMockManagerAPI() defer mock.close() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr, err := newManager(store, mock) require.NoError(t, err) @@ -1295,6 +1393,14 @@ func TestPaychGetMergeAddFundsCtxCancelOne(t *testing.T) { mock := newMockManagerAPI() defer mock.close() + act := &types.Actor{ + Code: builtin.AccountActorCodeID, + Head: cid.Cid{}, + Nonce: 0, + Balance: types.NewInt(20), + } + mock.setPaychState(ch, act, paychmock.NewMockPayChState(from, to, abi.ChainEpoch(0), make(map[uint64]paych.LaneState))) + mgr, err := newManager(store, mock) require.NoError(t, err) diff --git a/paychmgr/simple.go b/paychmgr/simple.go index ef0f81b8748..56bea0d27f8 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -436,7 +436,7 @@ func (ca *channelAccessor) processTask(merged *mergedFundsReq, amt, avail types. // Get the payment channel for the from/to addresses. // Note: It's ok if we get ErrChannelNotTracked. It just means we need to // create a channel. - channelInfo, err := ca.store.OutboundActiveByFromTo(ctx, ca.from, ca.to) + channelInfo, err := ca.store.OutboundActiveByFromTo(ctx, ca.api, ca.from, ca.to) if err != nil && err != ErrChannelNotTracked { return &paychFundsRes{err: err} } diff --git a/paychmgr/store.go b/paychmgr/store.go index d5c8e198049..7d6ee32be67 100644 --- a/paychmgr/store.go +++ b/paychmgr/store.go @@ -6,9 +6,8 @@ import ( "errors" "fmt" - "golang.org/x/xerrors" - "github.com/google/uuid" + "golang.org/x/xerrors" "github.com/filecoin-project/lotus/chain/types" @@ -380,7 +379,7 @@ func (ps *Store) GetMessage(ctx context.Context, mcid cid.Cid) (*MsgInfo, error) // OutboundActiveByFromTo looks for outbound channels that have not been // settled, with the given from / to addresses -func (ps *Store) OutboundActiveByFromTo(ctx context.Context, from address.Address, to address.Address) (*ChannelInfo, error) { +func (ps *Store) OutboundActiveByFromTo(ctx context.Context, sma stateManagerAPI, from address.Address, to address.Address) (*ChannelInfo, error) { return ps.findChan(ctx, func(ci *ChannelInfo) bool { if ci.Direction != DirOutbound { return false @@ -388,6 +387,21 @@ func (ps *Store) OutboundActiveByFromTo(ctx context.Context, from address.Addres if ci.Settling { return false } + + if ci.Channel != nil { + _, st, err := sma.GetPaychState(ctx, *ci.Channel, nil) + if err != nil { + return false + } + sat, err := st.SettlingAt() + if err != nil { + return false + } + if sat != 0 { + return false + } + } + return ci.Control == from && ci.Target == to }) } From f61eb23f8f6ff69af8b83fe3e398c6a40177eada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Mon, 14 Feb 2022 19:56:02 +0100 Subject: [PATCH 16/24] api: separate method for paych funding --- api/api_full.go | 9 +++--- api/mocks/mock_full.go | 15 ++++++++++ api/proxy_gen.go | 13 ++++++++ api/v0api/v1_wrapper.go | 5 +--- build/openrpc/full.json.gz | Bin 26979 -> 27021 bytes build/openrpc/miner.json.gz | Bin 12903 -> 12906 bytes build/openrpc/worker.json.gz | Bin 3960 -> 3959 bytes cli/paych.go | 12 +++++--- documentation/en/api-v1-unstable-methods.md | 31 ++++++++++++++++++-- markets/retrievaladapter/client.go | 1 - node/impl/paych/paych.go | 19 ++++++++---- paychmgr/manager.go | 12 +++++++- paychmgr/simple.go | 6 ++-- 13 files changed, 98 insertions(+), 25 deletions(-) diff --git a/api/api_full.go b/api/api_full.go index 853d4928390..6fd34ddfbab 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -690,14 +690,16 @@ type FullNode interface { // The Paych methods are for interacting with and managing payment channels // PaychGet gets or creates a payment channel between address pair - // - If opts.Reserve is false, the specified amount will be added to the channel through on-chain send for future use - // - If opts.Reserve is true, the specified amount will be reserved for use. If there aren't enough non-reserved funds + // The specified amount will be reserved for use. If there aren't enough non-reserved funds // available, funds will be added through an on-chain message. // - When opts.OffChain is true, this call will not cause any messages to be sent to the chain (no automatic // channel creation/funds adding). If the operation can't be performed without sending a message an error will be // returned. Note that even when this option is specified, this call can be blocked by previous operations on the // channel waiting for on-chain operations. - PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt, opts PaychGetOpts) (*ChannelInfo, error) //perm:sign + PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt, opts PaychGetOpts) (*ChannelInfo, error) //perm:sign + // PaychFund gets or creates a payment channel between address pair. + // The specified amount will be added to the channel through on-chain send for future use + PaychFund(ctx context.Context, from, to address.Address, amt types.BigInt) (*ChannelInfo, error) //perm:sign PaychGetWaitReady(context.Context, cid.Cid) (address.Address, error) //perm:sign PaychAvailableFunds(ctx context.Context, ch address.Address) (*ChannelAvailableFunds, error) //perm:sign PaychAvailableFundsByFromTo(ctx context.Context, from, to address.Address) (*ChannelAvailableFunds, error) //perm:sign @@ -837,7 +839,6 @@ const ( ) type PaychGetOpts struct { - Reserve bool OffChain bool } diff --git a/api/mocks/mock_full.go b/api/mocks/mock_full.go index 5d42ba3c918..ab120abdd22 100644 --- a/api/mocks/mock_full.go +++ b/api/mocks/mock_full.go @@ -1975,6 +1975,21 @@ func (mr *MockFullNodeMockRecorder) PaychCollect(arg0, arg1 interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaychCollect", reflect.TypeOf((*MockFullNode)(nil).PaychCollect), arg0, arg1) } +// PaychFund mocks base method. +func (m *MockFullNode) PaychFund(arg0 context.Context, arg1, arg2 address.Address, arg3 big.Int) (*api.ChannelInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaychFund", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*api.ChannelInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaychFund indicates an expected call of PaychFund. +func (mr *MockFullNodeMockRecorder) PaychFund(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaychFund", reflect.TypeOf((*MockFullNode)(nil).PaychFund), arg0, arg1, arg2, arg3) +} + // PaychGet mocks base method. func (m *MockFullNode) PaychGet(arg0 context.Context, arg1, arg2 address.Address, arg3 big.Int, arg4 api.PaychGetOpts) (*api.ChannelInfo, error) { m.ctrl.T.Helper() diff --git a/api/proxy_gen.go b/api/proxy_gen.go index c90ec4cddb5..0f849343058 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -306,6 +306,8 @@ type FullNodeStruct struct { PaychCollect func(p0 context.Context, p1 address.Address) (cid.Cid, error) `perm:"sign"` + PaychFund func(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt) (*ChannelInfo, error) `perm:"sign"` + PaychGet func(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt, p4 PaychGetOpts) (*ChannelInfo, error) `perm:"sign"` PaychGetWaitReady func(p0 context.Context, p1 cid.Cid) (address.Address, error) `perm:"sign"` @@ -2185,6 +2187,17 @@ func (s *FullNodeStub) PaychCollect(p0 context.Context, p1 address.Address) (cid return *new(cid.Cid), ErrNotSupported } +func (s *FullNodeStruct) PaychFund(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt) (*ChannelInfo, error) { + if s.Internal.PaychFund == nil { + return nil, ErrNotSupported + } + return s.Internal.PaychFund(p0, p1, p2, p3) +} + +func (s *FullNodeStub) PaychFund(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt) (*ChannelInfo, error) { + return nil, ErrNotSupported +} + func (s *FullNodeStruct) PaychGet(p0 context.Context, p1 address.Address, p2 address.Address, p3 types.BigInt, p4 PaychGetOpts) (*ChannelInfo, error) { if s.Internal.PaychGet == nil { return nil, ErrNotSupported diff --git a/api/v0api/v1_wrapper.go b/api/v0api/v1_wrapper.go index 1c22eb920ad..3f2dd837391 100644 --- a/api/v0api/v1_wrapper.go +++ b/api/v0api/v1_wrapper.go @@ -338,10 +338,7 @@ func (w *WrapperV1Full) clientRetrieve(ctx context.Context, order RetrievalOrder } func (w *WrapperV1Full) PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt) (*api.ChannelInfo, error) { - return w.FullNode.PaychGet(ctx, from, to, amt, api.PaychGetOpts{ - Reserve: true, // v0 always reserves - OffChain: false, - }) + return w.FullNode.PaychFund(ctx, from, to, amt) } var _ FullNode = &WrapperV1Full{} diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index b486a5123c56815d472f15994443f8b2b91bb98b..676ffbf4fe668ccd3033379c9a6d6d2fbafb78c8 100644 GIT binary patch delta 23020 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 bs7O=V`{O5aBzyf6009609MB4oXG{eEiOJL` diff --git a/build/openrpc/miner.json.gz b/build/openrpc/miner.json.gz index 5e9c824f58ab6dd0fcb0a1415e25b8e609bb088a..eef2701f28055a067fc6541177f878cc1d65172b 100644 GIT binary patch delta 9997 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 Date: Mon, 14 Feb 2022 20:07:13 +0100 Subject: [PATCH 17/24] paychmgr: Review comments Co-authored-by: Hannah Howard Co-authored-by: dirkmc --- api/api_full.go | 2 +- documentation/en/default-lotus-config.toml | 2 +- node/config/types.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/api_full.go b/api/api_full.go index 6fd34ddfbab..d6f657ad01e 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -866,7 +866,7 @@ type ChannelAvailableFunds struct { PendingAmt types.BigInt // AvailableAmt is part of ConfirmedAmt that is available for use (pre-allocated) AvailableAmt types.BigInt - // PendingAvailableAmt is the amount of available funds that are pending confirmation on-chain +// PendingAvailableAmt is the amount of funds that are pending confirmation on-chain that will become available once confirmed PendingAvailableAmt types.BigInt // PendingWaitSentinel can be used with PaychGetWaitReady to wait for // confirmation of pending funds diff --git a/documentation/en/default-lotus-config.toml b/documentation/en/default-lotus-config.toml index 7ef8f6309a1..ad917814b0d 100644 --- a/documentation/en/default-lotus-config.toml +++ b/documentation/en/default-lotus-config.toml @@ -121,7 +121,7 @@ # env var: LOTUS_CLIENT_SIMULTANEOUSTRANSFERSFORRETRIEVAL #SimultaneousTransfersForRetrieval = 20 - # Require that retrievals perform no on-chain retrievals. Paid retrievals + # Require that retrievals perform no on-chain operations. Paid retrievals # without existing payment channels with available funds will fail instead # of automatically performing on-chain operations. # diff --git a/node/config/types.go b/node/config/types.go index 762b8b6eb27..b073735d32e 100644 --- a/node/config/types.go +++ b/node/config/types.go @@ -388,7 +388,7 @@ type Client struct { // and storage providers for retrieval deals SimultaneousTransfersForRetrieval uint64 - // Require that retrievals perform no on-chain retrievals. Paid retrievals + // Require that retrievals perform no on-chain operations. Paid retrievals // without existing payment channels with available funds will fail instead // of automatically performing on-chain operations. OffChainRetrieval bool From 36a193484568ecbbf59763239c9943683f1bfff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Mon, 14 Feb 2022 20:16:30 +0100 Subject: [PATCH 18/24] paychmgr: Fix tests after api changes --- itests/paych_api_test.go | 1 - node/impl/paych/paych.go | 10 ++++++++-- paychmgr/manager.go | 9 ++------- paychmgr/paychget_test.go | 9 ++++----- paychmgr/simple.go | 6 +++--- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/itests/paych_api_test.go b/itests/paych_api_test.go index 074551a8376..1a39fd606a7 100644 --- a/itests/paych_api_test.go +++ b/itests/paych_api_test.go @@ -66,7 +66,6 @@ func TestPaymentChannelsAPI(t *testing.T) { channelAmt := int64(7000) channelInfo, err := paymentCreator.PaychGet(ctx, createrAddr, receiverAddr, abi.NewTokenAmount(channelAmt), api.PaychGetOpts{ - Reserve: true, OffChain: false, }) require.NoError(t, err) diff --git a/node/impl/paych/paych.go b/node/impl/paych/paych.go index 28e50625e93..d338c6032fd 100644 --- a/node/impl/paych/paych.go +++ b/node/impl/paych/paych.go @@ -23,7 +23,10 @@ type PaychAPI struct { } func (a *PaychAPI) PaychGet(ctx context.Context, from, to address.Address, amt types.BigInt, opts api.PaychGetOpts) (*api.ChannelInfo, error) { - ch, mcid, err := a.PaychMgr.GetPaych(ctx, from, to, amt, true, opts) + ch, mcid, err := a.PaychMgr.GetPaych(ctx, from, to, amt, paychmgr.GetOpts{ + Reserve: true, + OffChain: opts.OffChain, + }) if err != nil { return nil, err } @@ -35,7 +38,10 @@ func (a *PaychAPI) PaychGet(ctx context.Context, from, to address.Address, amt t } func (a *PaychAPI) PaychFund(ctx context.Context, from, to address.Address, amt types.BigInt) (*api.ChannelInfo, error) { - ch, mcid, err := a.PaychMgr.GetPaych(ctx, from, to, amt, false, api.PaychGetOpts{OffChain: false}) + ch, mcid, err := a.PaychMgr.GetPaych(ctx, from, to, amt, paychmgr.GetOpts{ + Reserve: false, + OffChain: false, + }) if err != nil { return nil, err } diff --git a/paychmgr/manager.go b/paychmgr/manager.go index 46466266d7b..081ef4c5d19 100644 --- a/paychmgr/manager.go +++ b/paychmgr/manager.go @@ -101,17 +101,12 @@ func (pm *Manager) Stop() error { return nil } -type getOpts struct { +type GetOpts struct { Reserve bool OffChain bool } -func (pm *Manager) GetPaych(ctx context.Context, from, to address.Address, amt types.BigInt, reserve bool, o api.PaychGetOpts) (address.Address, cid.Cid, error) { - opts := getOpts{ - Reserve: reserve, - OffChain: o.OffChain, - } - +func (pm *Manager) GetPaych(ctx context.Context, from, to address.Address, amt types.BigInt, opts GetOpts) (address.Address, cid.Cid, error) { if !opts.Reserve && opts.OffChain { return address.Undef, cid.Undef, xerrors.Errorf("can't fund payment channels without on-chain operations") } diff --git a/paychmgr/paychget_test.go b/paychmgr/paychget_test.go index a508fdfc96a..0688301e883 100644 --- a/paychmgr/paychget_test.go +++ b/paychmgr/paychget_test.go @@ -19,26 +19,25 @@ import ( init2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/init" tutils "github.com/filecoin-project/specs-actors/v2/support/testing" - "github.com/filecoin-project/lotus/api" lotusinit "github.com/filecoin-project/lotus/chain/actors/builtin/init" "github.com/filecoin-project/lotus/chain/actors/builtin/paych" paychmock "github.com/filecoin-project/lotus/chain/actors/builtin/paych/mock" "github.com/filecoin-project/lotus/chain/types" ) -var onChainReserve = api.PaychGetOpts{ +var onChainReserve = GetOpts{ Reserve: true, OffChain: false, } -var onChainNoReserve = api.PaychGetOpts{ +var onChainNoReserve = GetOpts{ Reserve: false, OffChain: false, } -var offChainReserve = api.PaychGetOpts{ +var offChainReserve = GetOpts{ Reserve: true, OffChain: true, } -var offChainNoReserve = api.PaychGetOpts{ +var offChainNoReserve = GetOpts{ Reserve: false, OffChain: true, } diff --git a/paychmgr/simple.go b/paychmgr/simple.go index cc6a9286180..0eb1316af57 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -33,14 +33,14 @@ type fundsReq struct { ctx context.Context promise chan *paychFundsRes amt types.BigInt - opts getOpts + opts GetOpts lk sync.Mutex // merge parent, if this req is part of a merge merge *mergedFundsReq } -func newFundsReq(ctx context.Context, amt types.BigInt, opts getOpts) *fundsReq { +func newFundsReq(ctx context.Context, amt types.BigInt, opts GetOpts) *fundsReq { promise := make(chan *paychFundsRes, 1) return &fundsReq{ ctx: ctx, @@ -251,7 +251,7 @@ func (m *mergedFundsReq) failOffChainNoChannel(from, to address.Address) (*paych // address and the CID of the new add funds message. // If an operation returns an error, subsequent waiting operations will still // be attempted. -func (ca *channelAccessor) getPaych(ctx context.Context, amt types.BigInt, opts getOpts) (address.Address, cid.Cid, error) { +func (ca *channelAccessor) getPaych(ctx context.Context, amt types.BigInt, opts GetOpts) (address.Address, cid.Cid, error) { // Add the request to add funds to a queue and wait for the result freq := newFundsReq(ctx, amt, opts) ca.enqueue(ctx, freq) From f9485eb9c253e3faa9304d7ec963233a14e66867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Mon, 14 Feb 2022 20:42:45 +0100 Subject: [PATCH 19/24] Update paychmgr/store.go Co-authored-by: dirkmc --- paychmgr/store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paychmgr/store.go b/paychmgr/store.go index 7d6ee32be67..f3c67e5650b 100644 --- a/paychmgr/store.go +++ b/paychmgr/store.go @@ -73,7 +73,7 @@ type ChannelInfo struct { // has locally been added to the channel. It should reflect the channel's // Balance on chain as long as all operations occur on the same datastore. Amount types.BigInt - // AvailableAmount indicates how much afil is non-reverved + // AvailableAmount indicates how much afil is non-reserved AvailableAmount types.BigInt // PendingAvailableAmount is available amount that we're awaiting confirmation of PendingAvailableAmount types.BigInt From da5ae1efe178717f1c97401f67e3fd298cb74f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Mon, 14 Feb 2022 21:11:31 +0100 Subject: [PATCH 20/24] paychmgr: erlier checks in completeAmount --- paychmgr/simple.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/paychmgr/simple.go b/paychmgr/simple.go index 0eb1316af57..5a23c7f2c05 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -179,6 +179,11 @@ func (m *mergedFundsReq) completeAmount(avail types.BigInt, channelInfo *Channel break } + // don't try to fill inactive requests + if !r.isActive() { + continue + } + if r.amt.GreaterThan(types.BigSub(avail, used)) { // requests are sorted by amount ascending, so if we hit this, there aren't any more requests we can fill @@ -198,11 +203,6 @@ func (m *mergedFundsReq) completeAmount(avail types.BigInt, channelInfo *Channel break } - // don't try to fill inactive requests - if !r.isActive() { - continue - } - used = types.BigAdd(used, r.amt) r.onComplete(&paychFundsRes{channel: *channelInfo.Channel}) next = i + 1 From 60b9acc09d7d7fea061ce89ba1c6ae3fe5ae1ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Mon, 14 Feb 2022 21:16:41 +0100 Subject: [PATCH 21/24] gen --- api/api_full.go | 2 +- node/config/doc_gen.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/api_full.go b/api/api_full.go index d6f657ad01e..b621dd9fbb9 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -866,7 +866,7 @@ type ChannelAvailableFunds struct { PendingAmt types.BigInt // AvailableAmt is part of ConfirmedAmt that is available for use (pre-allocated) AvailableAmt types.BigInt -// PendingAvailableAmt is the amount of funds that are pending confirmation on-chain that will become available once confirmed + // PendingAvailableAmt is the amount of funds that are pending confirmation on-chain that will become available once confirmed PendingAvailableAmt types.BigInt // PendingWaitSentinel can be used with PaychGetWaitReady to wait for // confirmation of pending funds diff --git a/node/config/doc_gen.go b/node/config/doc_gen.go index d160643c271..2a7f63180ce 100644 --- a/node/config/doc_gen.go +++ b/node/config/doc_gen.go @@ -109,7 +109,7 @@ and storage providers for retrieval deals`, Name: "OffChainRetrieval", Type: "bool", - Comment: `Require that retrievals perform no on-chain retrievals. Paid retrievals + Comment: `Require that retrievals perform no on-chain operations. Paid retrievals without existing payment channels with available funds will fail instead of automatically performing on-chain operations.`, }, From e1a36058b44089ce1da97f3a85eaf07491057cf8 Mon Sep 17 00:00:00 2001 From: hannahhoward Date: Tue, 15 Feb 2022 21:52:06 -0800 Subject: [PATCH 22/24] feat(markets): update markets to simplify client adapter --- go.mod | 2 +- go.sum | 4 ++-- markets/retrievaladapter/client.go | 33 ------------------------------ 3 files changed, 3 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index f22400ded5a..6b6191de617 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/filecoin-project/go-data-transfer v1.14.0 github.com/filecoin-project/go-fil-commcid v0.1.0 github.com/filecoin-project/go-fil-commp-hashhash v0.1.0 - github.com/filecoin-project/go-fil-markets v1.19.0 + github.com/filecoin-project/go-fil-markets v1.19.1 github.com/filecoin-project/go-jsonrpc v0.1.5 github.com/filecoin-project/go-padreader v0.0.1 github.com/filecoin-project/go-paramfetch v0.0.4 diff --git a/go.sum b/go.sum index 6b725b5df74..3df9f4eb069 100644 --- a/go.sum +++ b/go.sum @@ -327,8 +327,8 @@ github.com/filecoin-project/go-fil-commcid v0.1.0 h1:3R4ds1A9r6cr8mvZBfMYxTS88Oq github.com/filecoin-project/go-fil-commcid v0.1.0/go.mod h1:Eaox7Hvus1JgPrL5+M3+h7aSPHc0cVqpSxA+TxIEpZQ= github.com/filecoin-project/go-fil-commp-hashhash v0.1.0 h1:imrrpZWEHRnNqqv0tN7LXep5bFEVOVmQWHJvl2mgsGo= github.com/filecoin-project/go-fil-commp-hashhash v0.1.0/go.mod h1:73S8WSEWh9vr0fDJVnKADhfIv/d6dCbAGaAGWbdJEI8= -github.com/filecoin-project/go-fil-markets v1.19.0 h1:kap2q2wTM6tfkVO5gMA5DD9GUeTvkDhMfhjCtEwMDM8= -github.com/filecoin-project/go-fil-markets v1.19.0/go.mod h1:qsb3apmo4RSJYCEq40QxVdU7UZospN6nFJLOBHuaIbc= +github.com/filecoin-project/go-fil-markets v1.19.1 h1:o5sziAp8zCsvIg3KYMgIpwm8gyOl4MDzEKEf0Qq5L3U= +github.com/filecoin-project/go-fil-markets v1.19.1/go.mod h1:qsb3apmo4RSJYCEq40QxVdU7UZospN6nFJLOBHuaIbc= github.com/filecoin-project/go-hamt-ipld v0.1.5 h1:uoXrKbCQZ49OHpsTCkrThPNelC4W3LPEk0OrS/ytIBM= github.com/filecoin-project/go-hamt-ipld v0.1.5/go.mod h1:6Is+ONR5Cd5R6XZoCse1CWaXZc0Hdb/JeX+EQCQzX24= github.com/filecoin-project/go-hamt-ipld/v2 v2.0.0 h1:b3UDemBYN2HNfk3KOXNuxgTTxlWi3xVvbQP0IT38fvM= diff --git a/markets/retrievaladapter/client.go b/markets/retrievaladapter/client.go index b5535bcfc43..74f02570b9e 100644 --- a/markets/retrievaladapter/client.go +++ b/markets/retrievaladapter/client.go @@ -9,8 +9,6 @@ import ( "github.com/filecoin-project/go-state-types/abi" "github.com/ipfs/go-cid" "github.com/multiformats/go-multiaddr" - mh "github.com/multiformats/go-multihash" - "golang.org/x/xerrors" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/actors/builtin/paych" @@ -19,27 +17,6 @@ import ( payapi "github.com/filecoin-project/lotus/node/impl/paych" ) -func mkPaychReusedCid(addr address.Address) cid.Cid { - c, err := cid.V1Builder{Codec: cid.Raw, MhType: mh.IDENTITY}.Sum(addr.Bytes()) - if err != nil { - panic(err) - } - return c -} - -func extractPaychReusedCid(c cid.Cid) (address.Address, error) { - if c.Prefix().Codec != cid.Raw { - return address.Undef, nil - } - - h, err := mh.Decode(c.Hash()) - if err != nil { - return address.Address{}, err - } - - return address.NewFromBytes(h.Digest) -} - type retrievalClientNode struct { forceOffChain bool @@ -72,9 +49,6 @@ func (rcn *retrievalClientNode) GetOrCreatePaymentChannel(ctx context.Context, c log.Errorw("paych get failed", "error", err) return address.Undef, cid.Undef, err } - if ci.WaitSentinel == cid.Undef { - return ci.Channel, mkPaychReusedCid(ci.Channel), nil - } return ci.Channel, ci.WaitSentinel, nil } @@ -112,13 +86,6 @@ func (rcn *retrievalClientNode) GetChainHead(ctx context.Context) (shared.TipSet } func (rcn *retrievalClientNode) WaitForPaymentChannelReady(ctx context.Context, messageCID cid.Cid) (address.Address, error) { - maybeAddr, err := extractPaychReusedCid(messageCID) - if err != nil { - return address.Address{}, xerrors.Errorf("extract paych reused CID: %w", err) - } - if maybeAddr != address.Undef { - return maybeAddr, nil - } return rcn.payAPI.PaychGetWaitReady(ctx, messageCID) } From e961766e4b7c8813816371e8cda1d2e490160a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Wed, 16 Feb 2022 10:08:44 +0100 Subject: [PATCH 23/24] fix lotus-soup build --- testplans/lotus-soup/deals_e2e.go | 2 -- testplans/lotus-soup/go.mod | 2 +- testplans/lotus-soup/go.sum | 13 +++++++------ testplans/lotus-soup/paych/stress.go | 1 - 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/testplans/lotus-soup/deals_e2e.go b/testplans/lotus-soup/deals_e2e.go index d9be97f6cae..44eec2d7ab1 100644 --- a/testplans/lotus-soup/deals_e2e.go +++ b/testplans/lotus-soup/deals_e2e.go @@ -208,7 +208,6 @@ func initPaymentChannel(t *testkit.TestEnvironment, ctx context.Context, cl *tes t.RecordMessage("creating payment channel; from=%s, to=%s, funds=%d", cl.Wallet.Address, recv.WalletAddr, balance) channel, err := cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, balance, api.PaychGetOpts{ - Reserve: true, OffChain: false, }) if err != nil { @@ -234,7 +233,6 @@ func initPaymentChannel(t *testkit.TestEnvironment, ctx context.Context, cl *tes t.RecordMessage("reloading paych; now it should have an address") channel, err = cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, big.Zero(), api.PaychGetOpts{ - Reserve: true, OffChain: false, }) if err != nil { diff --git a/testplans/lotus-soup/go.mod b/testplans/lotus-soup/go.mod index f227452e7ae..94673c08119 100644 --- a/testplans/lotus-soup/go.mod +++ b/testplans/lotus-soup/go.mod @@ -9,7 +9,7 @@ require ( github.com/drand/drand v1.3.0 github.com/filecoin-project/go-address v0.0.6 github.com/filecoin-project/go-data-transfer v1.14.0 - github.com/filecoin-project/go-fil-markets v1.19.0 + github.com/filecoin-project/go-fil-markets v1.19.1 github.com/filecoin-project/go-jsonrpc v0.1.5 github.com/filecoin-project/go-state-types v0.1.3 github.com/filecoin-project/go-storedcounter v0.1.0 diff --git a/testplans/lotus-soup/go.sum b/testplans/lotus-soup/go.sum index 23877f87b44..62a41703a05 100644 --- a/testplans/lotus-soup/go.sum +++ b/testplans/lotus-soup/go.sum @@ -418,8 +418,8 @@ github.com/filecoin-project/go-fil-commcid v0.1.0 h1:3R4ds1A9r6cr8mvZBfMYxTS88Oq github.com/filecoin-project/go-fil-commcid v0.1.0/go.mod h1:Eaox7Hvus1JgPrL5+M3+h7aSPHc0cVqpSxA+TxIEpZQ= github.com/filecoin-project/go-fil-commp-hashhash v0.1.0 h1:imrrpZWEHRnNqqv0tN7LXep5bFEVOVmQWHJvl2mgsGo= github.com/filecoin-project/go-fil-commp-hashhash v0.1.0/go.mod h1:73S8WSEWh9vr0fDJVnKADhfIv/d6dCbAGaAGWbdJEI8= -github.com/filecoin-project/go-fil-markets v1.19.0 h1:kap2q2wTM6tfkVO5gMA5DD9GUeTvkDhMfhjCtEwMDM8= -github.com/filecoin-project/go-fil-markets v1.19.0/go.mod h1:qsb3apmo4RSJYCEq40QxVdU7UZospN6nFJLOBHuaIbc= +github.com/filecoin-project/go-fil-markets v1.19.1 h1:o5sziAp8zCsvIg3KYMgIpwm8gyOl4MDzEKEf0Qq5L3U= +github.com/filecoin-project/go-fil-markets v1.19.1/go.mod h1:qsb3apmo4RSJYCEq40QxVdU7UZospN6nFJLOBHuaIbc= github.com/filecoin-project/go-hamt-ipld v0.1.5 h1:uoXrKbCQZ49OHpsTCkrThPNelC4W3LPEk0OrS/ytIBM= github.com/filecoin-project/go-hamt-ipld v0.1.5/go.mod h1:6Is+ONR5Cd5R6XZoCse1CWaXZc0Hdb/JeX+EQCQzX24= github.com/filecoin-project/go-hamt-ipld/v2 v2.0.0 h1:b3UDemBYN2HNfk3KOXNuxgTTxlWi3xVvbQP0IT38fvM= @@ -473,8 +473,8 @@ github.com/filecoin-project/specs-actors/v7 v7.0.0-20211117170924-fd07a4c7dff9/g github.com/filecoin-project/specs-actors/v7 v7.0.0-20211222192039-c83bea50c402/go.mod h1:p6LIOFezA1rgRLMewbvdi3Pp6SAu+q9FtJ9CAleSjrE= github.com/filecoin-project/specs-actors/v7 v7.0.0-rc1 h1:FuDaXIbcw2hRsFI8SDTmsGGCE+NumpF6aiBoU/2X5W4= github.com/filecoin-project/specs-actors/v7 v7.0.0-rc1/go.mod h1:TA5FwCna+Yi36POaT7SLKXsgEDvJwc0V/L6ZsO19B9M= -github.com/filecoin-project/specs-storage v0.1.1-0.20211228030229-6d460d25a0c9 h1:oUYOvF7EvdXS0Zmk9mNkaB6Bu0l+WXBYPzVodKMiLug= -github.com/filecoin-project/specs-storage v0.1.1-0.20211228030229-6d460d25a0c9/go.mod h1:Tb88Zq+IBJbvAn3mS89GYj3jdRThBTE/771HCVZdRJU= +github.com/filecoin-project/specs-storage v0.2.0 h1:Y4UDv0apRQ3zI2GiPPubi8JblpUZZphEdaJUxCutfyg= +github.com/filecoin-project/specs-storage v0.2.0/go.mod h1:Tb88Zq+IBJbvAn3mS89GYj3jdRThBTE/771HCVZdRJU= github.com/filecoin-project/test-vectors/schema v0.0.5/go.mod h1:iQ9QXLpYWL3m7warwvK1JC/pTri8mnfEmKygNDqqY6E= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= @@ -1411,8 +1411,9 @@ github.com/libp2p/go-libp2p-record v0.1.2/go.mod h1:pal0eNcT5nqZaTV7UGhqeGqxFgGd github.com/libp2p/go-libp2p-record v0.1.3 h1:R27hoScIhQf/A8XJZ8lYpnqh9LatJ5YbHs28kCIfql0= github.com/libp2p/go-libp2p-record v0.1.3/go.mod h1:yNUff/adKIfPnYQXgp6FQmNu3gLJ6EMg7+/vv2+9pY4= github.com/libp2p/go-libp2p-resource-manager v0.1.0/go.mod h1:wJPNjeE4XQlxeidwqVY5G6DLOKqFK33u2n8blpl0I6Y= -github.com/libp2p/go-libp2p-resource-manager v0.1.3 h1:Umf0tW6WNXSb6Uoma0YT56azB5iikL/aeGAP7s7+f5o= github.com/libp2p/go-libp2p-resource-manager v0.1.3/go.mod h1:wJPNjeE4XQlxeidwqVY5G6DLOKqFK33u2n8blpl0I6Y= +github.com/libp2p/go-libp2p-resource-manager v0.1.4 h1:RcxMD0pytOUimx3BqTVs6IqItb3H5Qg44SD7XyT68lw= +github.com/libp2p/go-libp2p-resource-manager v0.1.4/go.mod h1:wJPNjeE4XQlxeidwqVY5G6DLOKqFK33u2n8blpl0I6Y= github.com/libp2p/go-libp2p-routing v0.0.1/go.mod h1:N51q3yTr4Zdr7V8Jt2JIktVU+3xBBylx1MZeVA6t1Ys= github.com/libp2p/go-libp2p-routing v0.1.0/go.mod h1:zfLhI1RI8RLEzmEaaPwzonRvXeeSHddONWkcTcB54nE= github.com/libp2p/go-libp2p-routing-helpers v0.2.3 h1:xY61alxJ6PurSi+MXbywZpelvuU4U4p/gPTxjqCqTzY= @@ -2941,4 +2942,4 @@ sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= \ No newline at end of file +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/testplans/lotus-soup/paych/stress.go b/testplans/lotus-soup/paych/stress.go index 2f90308d8ee..4f107bf5b67 100644 --- a/testplans/lotus-soup/paych/stress.go +++ b/testplans/lotus-soup/paych/stress.go @@ -125,7 +125,6 @@ func runSender(ctx context.Context, t *testkit.TestEnvironment, clients []*testk time.Sleep(20 * time.Second) channel, err := cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, channelAmt, api.PaychGetOpts{ - Reserve: true, OffChain: false, }) if err != nil { From 384999556c95b2638f1b5f17a3456c781bc90936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Wed, 16 Feb 2022 20:39:43 +0100 Subject: [PATCH 24/24] paychmgr: AvailableAmt -> NonReservedAmt --- api/api_full.go | 11 +++++++---- build/openrpc/full.json.gz | Bin 27021 -> 27027 bytes cli/paych.go | 2 +- documentation/en/api-v0-methods.md | 4 ++-- documentation/en/api-v1-unstable-methods.md | 4 ++-- paychmgr/manager.go | 2 +- paychmgr/simple.go | 2 +- 7 files changed, 14 insertions(+), 11 deletions(-) diff --git a/api/api_full.go b/api/api_full.go index b621dd9fbb9..4c4d6ebf936 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -859,20 +859,23 @@ type ChannelAvailableFunds struct { From address.Address // To is the to address of the channel To address.Address - // ConfirmedAmt is the amount of funds that have been confirmed on-chain - // for the channel + + // ConfirmedAmt is the total amount of funds that have been confirmed on-chain for the channel ConfirmedAmt types.BigInt // PendingAmt is the amount of funds that are pending confirmation on-chain PendingAmt types.BigInt - // AvailableAmt is part of ConfirmedAmt that is available for use (pre-allocated) - AvailableAmt types.BigInt + + // NonReservedAmt is part of ConfirmedAmt that is available for use (e.g. when the payment channel was pre-funded) + NonReservedAmt types.BigInt // PendingAvailableAmt is the amount of funds that are pending confirmation on-chain that will become available once confirmed PendingAvailableAmt types.BigInt + // PendingWaitSentinel can be used with PaychGetWaitReady to wait for // confirmation of pending funds PendingWaitSentinel *cid.Cid // QueuedAmt is the amount that is queued up behind a pending request QueuedAmt types.BigInt + // VoucherRedeemedAmt is the amount that is redeemed by vouchers on-chain // and in the local datastore VoucherReedeemedAmt types.BigInt diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index 53dbbc6c5cb8486ce2acc67f62bf4689c0f171bf..db9366703169bb100e0c4bd04e6047b4f8dee184 100644 GIT binary patch delta 26956 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 diff --git a/cli/paych.go b/cli/paych.go index cb9035024fd..92c1a13e31f 100644 --- a/cli/paych.go +++ b/cli/paych.go @@ -200,7 +200,7 @@ func paychStatus(writer io.Writer, avail *lapi.ChannelAvailableFunds) { {"From", avail.From.String()}, {"To", avail.To.String()}, {"Confirmed Amt", fmt.Sprintf("%s", types.FIL(avail.ConfirmedAmt))}, - {"Available Amt", fmt.Sprintf("%s", types.FIL(avail.AvailableAmt))}, + {"Available Amt", fmt.Sprintf("%s", types.FIL(avail.NonReservedAmt))}, {"Voucher Redeemed Amt", fmt.Sprintf("%s", types.FIL(avail.VoucherReedeemedAmt))}, {"Pending Amt", fmt.Sprintf("%s", types.FIL(avail.PendingAmt))}, {"Pending Available Amt", fmt.Sprintf("%s", types.FIL(avail.PendingAvailableAmt))}, diff --git a/documentation/en/api-v0-methods.md b/documentation/en/api-v0-methods.md index 2061d7fadb7..9444444f569 100644 --- a/documentation/en/api-v0-methods.md +++ b/documentation/en/api-v0-methods.md @@ -4061,7 +4061,7 @@ Response: "To": "f01234", "ConfirmedAmt": "0", "PendingAmt": "0", - "AvailableAmt": "0", + "NonReservedAmt": "0", "PendingAvailableAmt": "0", "PendingWaitSentinel": null, "QueuedAmt": "0", @@ -4090,7 +4090,7 @@ Response: "To": "f01234", "ConfirmedAmt": "0", "PendingAmt": "0", - "AvailableAmt": "0", + "NonReservedAmt": "0", "PendingAvailableAmt": "0", "PendingWaitSentinel": null, "QueuedAmt": "0", diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index 966a0348c49..22e85901c9a 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -4457,7 +4457,7 @@ Response: "To": "f01234", "ConfirmedAmt": "0", "PendingAmt": "0", - "AvailableAmt": "0", + "NonReservedAmt": "0", "PendingAvailableAmt": "0", "PendingWaitSentinel": null, "QueuedAmt": "0", @@ -4486,7 +4486,7 @@ Response: "To": "f01234", "ConfirmedAmt": "0", "PendingAmt": "0", - "AvailableAmt": "0", + "NonReservedAmt": "0", "PendingAvailableAmt": "0", "PendingWaitSentinel": null, "QueuedAmt": "0", diff --git a/paychmgr/manager.go b/paychmgr/manager.go index 081ef4c5d19..ea77c67efbe 100644 --- a/paychmgr/manager.go +++ b/paychmgr/manager.go @@ -151,7 +151,7 @@ func (pm *Manager) AvailableFundsByFromTo(ctx context.Context, from address.Addr To: to, ConfirmedAmt: types.NewInt(0), PendingAmt: types.NewInt(0), - AvailableAmt: types.NewInt(0), + NonReservedAmt: types.NewInt(0), PendingAvailableAmt: types.NewInt(0), PendingWaitSentinel: nil, QueuedAmt: types.NewInt(0), diff --git a/paychmgr/simple.go b/paychmgr/simple.go index 5a23c7f2c05..3d0992efec7 100644 --- a/paychmgr/simple.go +++ b/paychmgr/simple.go @@ -417,7 +417,7 @@ func (ca *channelAccessor) currentAvailableFunds(ctx context.Context, channelID To: channelInfo.to(), ConfirmedAmt: channelInfo.Amount, PendingAmt: channelInfo.PendingAmount, - AvailableAmt: channelInfo.AvailableAmount, + NonReservedAmt: channelInfo.AvailableAmount, PendingAvailableAmt: channelInfo.PendingAvailableAmount, PendingWaitSentinel: waitSentinel, QueuedAmt: queuedAmt,