Payment Channels
-
State
stable
-
Theory Audit
wip
-
Edit this section
-
section-systems.filecoin_token.payment_channels
-
State
stable
-
Theory Audit
wip
- Edit this section
-
section-systems.filecoin_token.payment_channels
Payment channels are generally used as a mechanism to increase the scalability of blockchains and enable users to transact without involving (i.e., publishing their transactions on) the blockchain, which: i) increases the load of the system, and ii) incurs gas costs for the user. Payment channels generally use a smart contract as an agreement between the two participants. In the Filecoin blockchain Payment Channels are realised by the paychActor
.
The goal of the Payment Channel Actor specified here is to enable a series of off-chain microtransactions for applications built on top of Filecoin to be reconciled on-chain at a later time with fewer messages that involve the blockchain. Payment channels are already used in the Retrieval Market of the Filecoin Network, but their applicability is not constrained within this use-case only. Hence, here, we provide a detailed description of Payment Channels in the Filecoin network and then describe how Payment Channels are used in the specific case of the Filecoin Retrieval Market.
The payment channel actor can be used to open long-lived, flexible payment channels between users. Filecoin payment channels are uni-directional and can be funded by adding to their balance. Given the context of uni-directional payment channels, we define the payment channel sender as the party that receives some service, creates the channel, deposits funds and sends payments (hence the term payment channel sender). The payment channel recipient, on the other hand is defined as the party that provides services and receives payment for the services delivered (hence the term payment channel recipient). The fact that payment channels are uni-directional means that only the payment channel sender can add funds and the recipient can receive funds. Payment channels are identified by a unique address, as is the case with all Filecoin actors.
The payment channel state structure looks like this:
// A given payment channel actor is established by From (the receipent of a service)
// to enable off-chain microtransactions to To (the provider of a service) to be reconciled
// and tallied on chain.
type State struct {
// Channel owner, who has created and funded the actor - the channel sender
From addr.Address
// Recipient of payouts from channel
To addr.Address
// Amount successfully redeemed through the payment channel, paid out on `Collect()`
ToSend abi.TokenAmount
// Height at which the channel can be `Collected`
SettlingAt abi.ChainEpoch
// Height before which the channel `ToSend` cannot be collected
MinSettleHeight abi.ChainEpoch
// Collections of lane states for the channel, maintained in ID order.
LaneStates []*LaneState
}
Before continuing with the details of the Payment Channel and its components and features, it is worth defining a few terms.
- Voucher: a signed message created by either of the two channel parties that updates the channel balance. To differentiate to the payment channel sender/recipient, we refer to the voucher parties as voucher sender/recipient, who might or might not be the same as the payment channel ones (i.e., the voucher sender might be either the payment channel recipient or the payment channel sender).
- Redeeming a voucher: the voucher MUST be submitted on-chain by the opposite party from the one that created it. Redeeming a voucher does not trigger movement of funds from the channel to the recipient’s account, but it does incur message/gas costs. Vouchers can be redeemed at any time up to
Collect
(see below), as long as it has got a higherNonce
than a previously submitted one. UpdateChannelState
: this is the process by which a voucher is redeemed, i.e., a voucher is submitted (but not cashed-out) on-chain.Settle
: this process starts closing the channel. It can be called by either the channel creator (sender) or the channel recipient.Collect
: with this process funds are eventually transferred from the payment channel sender to the payment channel recipient. This process incurs message/gas costs.
Vouchers
-
State
stable
-
Theory Audit
wip
-
Edit this section
-
section-systems.filecoin_token.payment_channels.vouchers
-
State
stable
-
Theory Audit
wip
- Edit this section
-
section-systems.filecoin_token.payment_channels.vouchers
Traditionally, in order to transact through a Payment Channel, the payment channel parties send to each other signed messages that update the balance of the channel. In Filecoin, these signed messages are called vouchers.
Throughout the interaction between the two parties, the channel sender (From
address) is sending vouchers to the recipient (To
address). The Value
included in the voucher indicates the value available for the receiving party to redeem. The Value
is based on the service that the payment channel recipient has provided to the payment channel sender. Either the payment channel recipient or the payment channel sender can Update
the balance of the channel and the balance ToSend
to the payment channel recipient (using a voucher), but the Update
(i.e., the voucher) has to be accepted by the other party before funds can be collected. Furthermore, the voucher has to be redeemed by the opposite party from the one that issued the voucher. The payment channel recipient can choose to Collect
this balance at any time incurring the corresponding gas cost.
Redeeming a voucher is not transferring funds from the payment channel to the recipient’s account. Instead, redeeming a voucher denotes the fact that some service worth of Value
has been provided by the payment channel recipient to the payment channel sender. It is not until the whole payment channel is collected that the funds are dispatched to the provider’s account.
This is the structure of the voucher:
// A voucher can be created and sent by any of the two parties. The `To` payment channel address can redeem the voucher and then `Collect` the funds.
type SignedVoucher struct {
// ChannelAddr is the address of the payment channel this signed voucher is valid for
ChannelAddr addr.Address
// TimeLockMin sets a min epoch before which the voucher cannot be redeemed
TimeLockMin abi.ChainEpoch
// TimeLockMax sets a max epoch beyond which the voucher cannot be redeemed
// TimeLockMax set to 0 means no timeout
TimeLockMax abi.ChainEpoch
// (optional) The SecretPreImage is used by `To` to validate
SecretPreimage []byte
// (optional) Extra can be specified by `From` to add a verification method to the voucher
Extra *ModVerifyParams
// Specifies which lane the Voucher is added to (will be created if does not exist)
Lane uint64
// Nonce is set by `From` to prevent redemption of stale vouchers on a lane
Nonce uint64
// Amount voucher can be redeemed for
Amount big.Int
// (optional) MinSettleHeight can extend channel MinSettleHeight if needed
MinSettleHeight abi.ChainEpoch
// (optional) Set of lanes to be merged into `Lane`
Merges []Merge
// Sender's signature over the voucher
Signature *crypto.Signature
}
Over the course of a transaction cycle, each participant in the payment channel can send Voucher
s to the other participant.
For instance, if the payment channel sender (From
address) has sent to the payment channel recipient (To
address) the following three vouchers (voucher_val, voucher_nonce)
for a lane with 100 FIL to be redeemed: (10, 1), (20, 2), (30, 3), then the recipient could choose to redeem (30, 3) bringing the lane’s value to 70 (100 - 30) and cancelling the preceding vouchers, i.e., they would not be able to redeem (10, 1) or (20, 2) anymore. However, they could redeem (20, 2), that is, 20 FIL, and then follow up with (30, 3) to redeem the remaining 10 FIL later.
It is worth highlighting that while the Nonce
is a strictly increasing value to denote the sequence of vouchers issued within the remit of a payment channel, the Value
is not a strictly increasing value. Decreasing Value
(although expected rarely) can be realized in cases of refunds that need to flow in the direction from the payment channel recipient to the payment channel sender. This can be the case when some bits arrive corrupted in the case of file retrieval, for instance.
Vouchers are signed by the party that creates them and are authenticated using a (Secret
, PreImage
) pair provided by the paying party (channel sender). If the PreImage
is indeed a pre-image of the Secret
when used as input to some given algorithm (typically a one-way function like a hash), the Voucher
is valid. The Voucher
itself contains the PreImage
but not the Secret
(communicated separately to the receiving party). This enables multi-hop payments since an intermediary cannot redeem a voucher on their own. Vouchers can also be used to update the minimum height at which a channel will be settled (i.e., closed), or have TimeLock
s to prevent voucher recipients from redeeming them too early. A channel can also have a MinCloseHeight
to prevent it being closed prematurely (e.g. before the payment channel recipient has collected funds) by the payment channel creator/sender.
Once their transactions have completed, either party can choose to Settle
(i.e., close) the channel. There is a 12hr period after Settle
during which either party can submit any outstanding vouchers. Once the vouchers are submitted, either party can then call Collect
. This will send the payment channel recipient the ToPay
amount from the channel, and the channel sender (From
address) will be refunded the remaining balance in the channel (if any).
Lanes
-
State
stable
-
Theory Audit
wip
-
Edit this section
-
section-systems.filecoin_token.payment_channels.lanes
-
State
stable
-
Theory Audit
wip
- Edit this section
-
section-systems.filecoin_token.payment_channels.lanes
In addition, payment channels in Filecoin can be split into lane
s created as part of updating the channel state with a payment voucher
. Each lane has an associated nonce
and amount of tokens it can be redeemed
for. Lanes can be thought of as transactions for several different services provided by the channel recipient to the channel sender. The nonce
plays the role of a sequence number of vouchers within a given lane, where a voucher with a higher nonce replaces a voucher with a lower nonce.
Payment channel lanes allow for a lot of accounting between parties to be done off-chain and reconciled via single updates to the payment channel. The multiple lanes enable two parties to use a single payment channel to adjudicate multiple independent sets of payments.
One example of such accounting is merging of lanes. When a pair of channel sender-recipient nodes have a payment channel established between them with many lanes, the channel recipient will have to pay gas cost for each one of the lanes in order to Collect
funds. Merging of lanes allow the channel recipient to send a “merge” request to the channel sender to request merging of (some of the) lanes and consolidate the funds. This way, the recipient can reduce the overall gas cost. As an incentive for the channel sender to accept the merge lane request, the channel recipient can ask for a lower total value to balance out the gas cost. For instance, if the recipient has collected vouchers worth of 10 FIL from two lanes, say 5 from each, and the gas cost of submitting the vouchers for these funds is 2, then it can ask for 9 from the creator if the latter accepts to merge the two lanes. This way, the channel sender pays less overall for the services it received and the channel recipient pays less gas cost to submit the voucher for the services they provided.
Lifecycle of a Payment Channel
-
State
stable
-
Theory Audit
wip
-
Edit this section
-
section-systems.filecoin_token.payment_channels.lifecycle-of-a-payment-channel
-
State
stable
-
Theory Audit
wip
- Edit this section
-
section-systems.filecoin_token.payment_channels.lifecycle-of-a-payment-channel
Summarising, we have the following sequence:
- Two parties agree to a series of transactions (for instance as part of file retrieval) with one party paying the other party up to some total sum of Filecoin over time. This is part of the deal-phase, it takes place off-chain and does not (at this stage) involve payment channels.
- The Payment Channel Actor is used, called the payment channel sender (who is the recipient of some service, e.g., file in case of file retrieval) to create the payment channel and deposit funds.
- Any of the two parties can create vouchers to send to the other party.
- The voucher recipient saves the voucher locally. Each voucher has to be submitted by the opposite party from the one that created the voucher.
- Either immediately or later, the voucher recipient “redeems” the voucher by submitting it to the chain, calling
UpdateChannelState
- The channel sender or the channel recipient
Settle
the payment channel. - 12-hour period to close the channel begins.
- If any of the two parties have outstanding (i.e., non-redeemed) vouchers, they should now submit the vouchers to the chain (there should be the option of this being done automatically). If the channel recipient so desires, they should send a “merge lanes” request to the sender.
- 12-hour period ends.
- Either the channel sender or the channel recipient calls
Collect
. - Funds are transferred to the channel recipient’s account and any unclaimed balance goes back to channel sender.
Payment Channels as part of the Filecoin Retrieval
-
State
stable
-
Theory Audit
wip
-
Edit this section
-
section-systems.filecoin_token.payment_channels.payment-channels-as-part-of-the-filecoin-retrieval
-
State
stable
-
Theory Audit
wip
- Edit this section
-
section-systems.filecoin_token.payment_channels.payment-channels-as-part-of-the-filecoin-retrieval
Payment Channels are used in the Filecoin Retrieval Market to enable efficient off-chain payments and accounting between parties for what is expected to be a series of microtransactions, as these occur during data retrieval.
In particular, given that there is no proving method provided for the act of sending data from a provider (miner) to a client, there is no trust anchor between the two. Therefore, in order to avoid mis-behaviour, Filecoin is making use of payment channels in order to realise a step-wise “data transfer <-> payment” relationship between the data provider and the client (data receiver). Clients issue requests for data that miners are responding to. The miner is entitled to ask for interim payments, the volume-oriented interval for which is agreed in the Deal phase. In order to facilitate this process, the Filecoin client is creating a payment channel once the provider has agreed on the proposed deal. The client should also lock monetary value in the payment channel equal to the one needed for retrieval of the entire block of data requested. Every time a provider is completing transfer of the pre-specified amount of data, they can request a payment. The client is responding to this payment with a voucher which the provider can redeem (immediately or later), as per the process described earlier.
package paychmgr
import (
"context"
"errors"
"fmt"
"github.com/ipfs/go-cid"
"golang.org/x/xerrors"
"github.com/filecoin-project/go-address"
cborutil "github.com/filecoin-project/go-cbor-util"
actorstypes "github.com/filecoin-project/go-state-types/actors"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-state-types/builtin/v8/paych"
"github.com/filecoin-project/lotus/api"
lpaych "github.com/filecoin-project/lotus/chain/actors/builtin/paych"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/lib/sigs"
)
// insufficientFundsErr indicates that there are not enough funds in the
// channel to create a voucher
type insufficientFundsErr interface {
Shortfall() types.BigInt
}
type ErrInsufficientFunds struct {
shortfall types.BigInt
}
func newErrInsufficientFunds(shortfall types.BigInt) *ErrInsufficientFunds {
return &ErrInsufficientFunds{shortfall: shortfall}
}
func (e *ErrInsufficientFunds) Error() string {
return fmt.Sprintf("not enough funds in channel to cover voucher - shortfall: %d", e.shortfall)
}
func (e *ErrInsufficientFunds) Shortfall() types.BigInt {
return e.shortfall
}
type laneState struct {
redeemed big.Int
nonce uint64
}
func (ls laneState) Redeemed() (big.Int, error) {
return ls.redeemed, nil
}
func (ls laneState) Nonce() (uint64, error) {
return ls.nonce, nil
}
// channelAccessor is used to simplify locking when accessing a channel
type channelAccessor struct {
from address.Address
to address.Address
// chctx is used by background processes (eg when waiting for things to be
// confirmed on chain)
chctx context.Context
sa *stateAccessor
api managerAPI
store *Store
lk *channelLock
fundsReqQueue []*fundsReq
msgListeners msgListeners
}
func newChannelAccessor(pm *Manager, from address.Address, to address.Address) *channelAccessor {
return &channelAccessor{
from: from,
to: to,
chctx: pm.ctx,
sa: pm.sa,
api: pm.pchapi,
store: pm.store,
lk: &channelLock{globalLock: &pm.lk},
msgListeners: newMsgListeners(),
}
}
func (ca *channelAccessor) messageBuilder(ctx context.Context, from address.Address) (lpaych.MessageBuilder, error) {
nwVersion, err := ca.api.StateNetworkVersion(ctx, types.EmptyTSK)
if err != nil {
return nil, err
}
av, err := actorstypes.VersionForNetwork(nwVersion)
if err != nil {
return nil, err
}
return lpaych.Message(av, from), nil
}
func (ca *channelAccessor) getChannelInfo(ctx context.Context, addr address.Address) (*ChannelInfo, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
return ca.store.ByAddress(ctx, addr)
}
func (ca *channelAccessor) outboundActiveByFromTo(ctx context.Context, from, to address.Address) (*ChannelInfo, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
return ca.store.OutboundActiveByFromTo(ctx, ca.api, from, to)
}
// createVoucher creates a voucher with the given specification, setting its
// nonce, signing the voucher and storing it in the local datastore.
// If there are not enough funds in the channel to create the voucher, returns
// the shortfall in funds.
func (ca *channelAccessor) createVoucher(ctx context.Context, ch address.Address, voucher paych.SignedVoucher) (*api.VoucherCreateResult, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
// Find the channel for the voucher
ci, err := ca.store.ByAddress(ctx, ch)
if err != nil {
return nil, xerrors.Errorf("failed to get channel info by address: %w", err)
}
// Set the voucher channel
sv := &voucher
sv.ChannelAddr = ch
// Get the next nonce on the given lane
sv.Nonce = ca.nextNonceForLane(ci, voucher.Lane)
// Sign the voucher
vb, err := sv.SigningBytes()
if err != nil {
return nil, xerrors.Errorf("failed to get voucher signing bytes: %w", err)
}
sig, err := ca.api.WalletSign(ctx, ci.Control, vb)
if err != nil {
return nil, xerrors.Errorf("failed to sign voucher: %w", err)
}
sv.Signature = sig
// Store the voucher
if _, err := ca.addVoucherUnlocked(ctx, ch, sv, types.NewInt(0)); err != nil {
// If there are not enough funds in the channel to cover the voucher,
// return a voucher create result with the shortfall
var ife insufficientFundsErr
if errors.As(err, &ife) {
return &api.VoucherCreateResult{
Shortfall: ife.Shortfall(),
}, nil
}
return nil, xerrors.Errorf("failed to persist voucher: %w", err)
}
return &api.VoucherCreateResult{Voucher: sv, Shortfall: types.NewInt(0)}, nil
}
func (ca *channelAccessor) nextNonceForLane(ci *ChannelInfo, lane uint64) uint64 {
var maxnonce uint64
for _, v := range ci.Vouchers {
if v.Voucher.Lane == lane {
if v.Voucher.Nonce > maxnonce {
maxnonce = v.Voucher.Nonce
}
}
}
return maxnonce + 1
}
func (ca *channelAccessor) checkVoucherValid(ctx context.Context, ch address.Address, sv *paych.SignedVoucher) (map[uint64]lpaych.LaneState, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
return ca.checkVoucherValidUnlocked(ctx, ch, sv)
}
func (ca *channelAccessor) checkVoucherValidUnlocked(ctx context.Context, ch address.Address, sv *paych.SignedVoucher) (map[uint64]lpaych.LaneState, error) {
if sv.ChannelAddr != ch {
return nil, xerrors.Errorf("voucher ChannelAddr doesn't match channel address, got %s, expected %s", sv.ChannelAddr, ch)
}
// check voucher is unlocked
if sv.Extra != nil {
return nil, xerrors.Errorf("voucher is Message Locked")
}
if sv.TimeLockMax != 0 {
return nil, xerrors.Errorf("voucher is Max Time Locked")
}
if sv.TimeLockMin != 0 {
return nil, xerrors.Errorf("voucher is Min Time Locked")
}
if len(sv.SecretHash) != 0 {
return nil, xerrors.Errorf("voucher is Hash Locked")
}
// Load payment channel actor state
act, pchState, err := ca.sa.loadPaychActorState(ctx, ch)
if err != nil {
return nil, err
}
// Load channel "From" account actor state
f, err := pchState.From()
if err != nil {
return nil, err
}
from, err := ca.api.ResolveToDeterministicAddress(ctx, f, nil)
if err != nil {
return nil, err
}
// verify voucher signature
vb, err := sv.SigningBytes()
if err != nil {
return nil, err
}
// TODO: technically, either party may create and sign a voucher.
// However, for now, we only accept them from the channel creator.
// More complex handling logic can be added later
if err := sigs.Verify(sv.Signature, from, vb); err != nil {
return nil, err
}
// Check the voucher against the highest known voucher nonce / value
laneStates, err := ca.laneState(ctx, pchState, ch)
if err != nil {
return nil, err
}
// If the new voucher nonce value is less than the highest known
// nonce for the lane
ls, lsExists := laneStates[sv.Lane]
if lsExists {
n, err := ls.Nonce()
if err != nil {
return nil, err
}
if sv.Nonce <= n {
return nil, fmt.Errorf("nonce too low")
}
// If the voucher amount is less than the highest known voucher amount
r, err := ls.Redeemed()
if err != nil {
return nil, err
}
if sv.Amount.LessThanEqual(r) {
return nil, fmt.Errorf("voucher amount is lower than amount for voucher with lower nonce")
}
}
// Total redeemed is the total redeemed amount for all lanes, including
// the new voucher
// eg
//
// lane 1 redeemed: 3
// lane 2 redeemed: 2
// voucher for lane 1: 5
//
// Voucher supersedes lane 1 redeemed, therefore
// effective lane 1 redeemed: 5
//
// lane 1: 5
// lane 2: 2
// -
// total: 7
totalRedeemed, err := ca.totalRedeemedWithVoucher(laneStates, sv)
if err != nil {
return nil, err
}
// Total required balance must not exceed actor balance
if act.Balance.LessThan(totalRedeemed) {
return nil, newErrInsufficientFunds(types.BigSub(totalRedeemed, act.Balance))
}
if len(sv.Merges) != 0 {
return nil, fmt.Errorf("dont currently support paych lane merges")
}
return laneStates, nil
}
func (ca *channelAccessor) checkVoucherSpendable(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, secret []byte) (bool, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
recipient, err := ca.getPaychRecipient(ctx, ch)
if err != nil {
return false, err
}
ci, err := ca.store.ByAddress(ctx, ch)
if err != nil {
return false, err
}
// Check if voucher has already been submitted
submitted, err := ci.wasVoucherSubmitted(sv)
if err != nil {
return false, err
}
if submitted {
return false, nil
}
mb, err := ca.messageBuilder(ctx, recipient)
if err != nil {
return false, err
}
mes, err := mb.Update(ch, sv, secret)
if err != nil {
return false, err
}
ret, err := ca.api.Call(ctx, mes, nil)
if err != nil {
return false, err
}
if ret.MsgRct.ExitCode != 0 {
return false, nil
}
return true, nil
}
func (ca *channelAccessor) getPaychRecipient(ctx context.Context, ch address.Address) (address.Address, error) {
_, state, err := ca.api.GetPaychState(ctx, ch, nil)
if err != nil {
return address.Address{}, err
}
return state.To()
}
func (ca *channelAccessor) addVoucher(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, minDelta types.BigInt) (types.BigInt, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
return ca.addVoucherUnlocked(ctx, ch, sv, minDelta)
}
func (ca *channelAccessor) addVoucherUnlocked(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, minDelta types.BigInt) (types.BigInt, error) {
ci, err := ca.store.ByAddress(ctx, ch)
if err != nil {
return types.BigInt{}, err
}
// Check if the voucher has already been added
for _, v := range ci.Vouchers {
eq, err := cborutil.Equals(sv, v.Voucher)
if err != nil {
return types.BigInt{}, err
}
if eq {
// Ignore the duplicate voucher.
log.Warnf("AddVoucher: voucher re-added")
return types.NewInt(0), nil
}
}
// Check voucher validity
laneStates, err := ca.checkVoucherValidUnlocked(ctx, ch, sv)
if err != nil {
return types.NewInt(0), err
}
// The change in value is the delta between the voucher amount and
// the highest previous voucher amount for the lane
laneState, exists := laneStates[sv.Lane]
redeemed := big.NewInt(0)
if exists {
redeemed, err = laneState.Redeemed()
if err != nil {
return types.NewInt(0), err
}
}
delta := types.BigSub(sv.Amount, redeemed)
if minDelta.GreaterThan(delta) {
return delta, xerrors.Errorf("addVoucher: supplied token amount too low; minD=%s, D=%s; laneAmt=%s; v.Amt=%s", minDelta, delta, redeemed, sv.Amount)
}
ci.Vouchers = append(ci.Vouchers, &VoucherInfo{
Voucher: sv,
})
if ci.NextLane <= sv.Lane {
ci.NextLane = sv.Lane + 1
}
return delta, ca.store.putChannelInfo(ctx, ci)
}
func (ca *channelAccessor) submitVoucher(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, secret []byte) (cid.Cid, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
ci, err := ca.store.ByAddress(ctx, ch)
if err != nil {
return cid.Undef, err
}
has, err := ci.hasVoucher(sv)
if err != nil {
return cid.Undef, err
}
// If the channel has the voucher
if has {
// Check that the voucher hasn't already been submitted
submitted, err := ci.wasVoucherSubmitted(sv)
if err != nil {
return cid.Undef, err
}
if submitted {
return cid.Undef, xerrors.Errorf("cannot submit voucher that has already been submitted")
}
}
mb, err := ca.messageBuilder(ctx, ci.Control)
if err != nil {
return cid.Undef, err
}
msg, err := mb.Update(ch, sv, secret)
if err != nil {
return cid.Undef, err
}
smsg, err := ca.api.MpoolPushMessage(ctx, msg, nil)
if err != nil {
return cid.Undef, err
}
// If the channel didn't already have the voucher
if !has {
// Add the voucher to the channel
ci.Vouchers = append(ci.Vouchers, &VoucherInfo{
Voucher: sv,
})
}
// Mark the voucher and any lower-nonce vouchers as having been submitted
err = ca.store.MarkVoucherSubmitted(ctx, ci, sv)
if err != nil {
return cid.Undef, err
}
return smsg.Cid(), nil
}
func (ca *channelAccessor) allocateLane(ctx context.Context, ch address.Address) (uint64, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
return ca.store.AllocateLane(ctx, ch)
}
func (ca *channelAccessor) listVouchers(ctx context.Context, ch address.Address) ([]*VoucherInfo, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
// TODO: just having a passthrough method like this feels odd. Seems like
// there should be some filtering we're doing here
return ca.store.VouchersForPaych(ctx, ch)
}
// laneState gets the LaneStates from chain, then applies all vouchers in
// the data store over the chain state
func (ca *channelAccessor) laneState(ctx context.Context, state lpaych.State, ch address.Address) (map[uint64]lpaych.LaneState, error) {
// TODO: we probably want to call UpdateChannelState with all vouchers to be fully correct
// (but technically don't need to)
laneCount, err := state.LaneCount()
if err != nil {
return nil, err
}
// Note: we use a map instead of an array to store laneStates because the
// client sets the lane ID (the index) and potentially they could use a
// very large index.
laneStates := make(map[uint64]lpaych.LaneState, laneCount)
err = state.ForEachLaneState(func(idx uint64, ls lpaych.LaneState) error {
laneStates[idx] = ls
return nil
})
if err != nil {
return nil, err
}
// Apply locally stored vouchers
vouchers, err := ca.store.VouchersForPaych(ctx, ch)
if err != nil && err != ErrChannelNotTracked {
return nil, err
}
for _, v := range vouchers {
for range v.Voucher.Merges {
return nil, xerrors.Errorf("paych merges not handled yet")
}
// Check if there is an existing laneState in the payment channel
// for this voucher's lane
ls, ok := laneStates[v.Voucher.Lane]
// If the voucher does not have a higher nonce than the existing
// laneState for this lane, ignore it
if ok {
n, err := ls.Nonce()
if err != nil {
return nil, err
}
if v.Voucher.Nonce < n {
continue
}
}
// Voucher has a higher nonce, so replace laneState with this voucher
laneStates[v.Voucher.Lane] = laneState{v.Voucher.Amount, v.Voucher.Nonce}
}
return laneStates, nil
}
// Get the total redeemed amount across all lanes, after applying the voucher
func (ca *channelAccessor) totalRedeemedWithVoucher(laneStates map[uint64]lpaych.LaneState, sv *paych.SignedVoucher) (big.Int, error) {
// TODO: merges
if len(sv.Merges) != 0 {
return big.Int{}, xerrors.Errorf("dont currently support paych lane merges")
}
total := big.NewInt(0)
for _, ls := range laneStates {
r, err := ls.Redeemed()
if err != nil {
return big.Int{}, err
}
total = big.Add(total, r)
}
lane, ok := laneStates[sv.Lane]
if ok {
// If the voucher is for an existing lane, and the voucher nonce
// is higher than the lane nonce
n, err := lane.Nonce()
if err != nil {
return big.Int{}, err
}
if sv.Nonce > n {
// Add the delta between the redeemed amount and the voucher
// amount to the total
r, err := lane.Redeemed()
if err != nil {
return big.Int{}, err
}
delta := big.Sub(sv.Amount, r)
total = big.Add(total, delta)
}
} else {
// If the voucher is *not* for an existing lane, just add its
// value (implicitly a new lane will be created for the voucher)
total = big.Add(total, sv.Amount)
}
return total, nil
}
func (ca *channelAccessor) settle(ctx context.Context, ch address.Address) (cid.Cid, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
ci, err := ca.store.ByAddress(ctx, ch)
if err != nil {
return cid.Undef, err
}
mb, err := ca.messageBuilder(ctx, ci.Control)
if err != nil {
return cid.Undef, err
}
msg, err := mb.Settle(ch)
if err != nil {
return cid.Undef, err
}
smgs, err := ca.api.MpoolPushMessage(ctx, msg, nil)
if err != nil {
return cid.Undef, err
}
ci.Settling = true
err = ca.store.putChannelInfo(ctx, ci)
if err != nil {
log.Errorf("Error marking channel as settled: %s", err)
}
return smgs.Cid(), err
}
func (ca *channelAccessor) collect(ctx context.Context, ch address.Address) (cid.Cid, error) {
ca.lk.Lock()
defer ca.lk.Unlock()
ci, err := ca.store.ByAddress(ctx, ch)
if err != nil {
return cid.Undef, err
}
mb, err := ca.messageBuilder(ctx, ci.Control)
if err != nil {
return cid.Undef, err
}
msg, err := mb.Collect(ch)
if err != nil {
return cid.Undef, err
}
smsg, err := ca.api.MpoolPushMessage(ctx, msg, nil)
if err != nil {
return cid.Undef, err
}
return smsg.Cid(), nil
}
type SignedVoucher struct {
// ChannelAddr is the address of the payment channel this signed voucher is valid for
ChannelAddr addr.Address
// TimeLockMin sets a min epoch before which the voucher cannot be redeemed
TimeLockMin abi.ChainEpoch
// TimeLockMax sets a max epoch beyond which the voucher cannot be redeemed
// TimeLockMax set to 0 means no timeout
TimeLockMax abi.ChainEpoch
// (optional) The SecretPreImage is used by `To` to validate
SecretPreimage []byte
// (optional) Extra can be specified by `From` to add a verification method to the voucher
Extra *ModVerifyParams
// Specifies which lane the Voucher merges into (will be created if does not exist)
Lane uint64
// Nonce is set by `From` to prevent redemption of stale vouchers on a lane
Nonce uint64
// Amount voucher can be redeemed for
Amount big.Int
// (optional) MinSettleHeight can extend channel MinSettleHeight if needed
MinSettleHeight abi.ChainEpoch
// (optional) Set of lanes to be merged into `Lane`
Merges []Merge
// Sender's signature over the voucher
Signature *crypto.Signature
}
package paych
import (
"bytes"
addr "github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-state-types/cbor"
"github.com/filecoin-project/go-state-types/exitcode"
paych0 "github.com/filecoin-project/specs-actors/actors/builtin/paych"
paych7 "github.com/filecoin-project/specs-actors/v7/actors/builtin/paych"
"github.com/ipfs/go-cid"
"github.com/filecoin-project/specs-actors/v8/actors/builtin"
"github.com/filecoin-project/specs-actors/v8/actors/runtime"
"github.com/filecoin-project/specs-actors/v8/actors/util/adt"
)
const (
ErrChannelStateUpdateAfterSettled = exitcode.FirstActorSpecificExitCode + iota
)
type Actor struct{}
func (a Actor) Exports() []interface{} {
return []interface{}{
builtin.MethodConstructor: a.Constructor,
2: a.UpdateChannelState,
3: a.Settle,
4: a.Collect,
}
}
func (a Actor) Code() cid.Cid {
return builtin.PaymentChannelActorCodeID
}
func (a Actor) State() cbor.Er {
return new(State)
}
var _ runtime.VMActor = Actor{}
//type ConstructorParams struct {
// From addr.Address // Payer
// To addr.Address // Payee
//}
type ConstructorParams = paych0.ConstructorParams
// Constructor creates a payment channel actor. See State for meaning of params.
func (pca *Actor) Constructor(rt runtime.Runtime, params *ConstructorParams) *abi.EmptyValue {
// Only InitActor can create a payment channel actor. It creates the actor on
// behalf of the payer/payee.
rt.ValidateImmediateCallerType(builtin.InitActorCodeID)
// check that both parties are capable of signing vouchers
to, err := pca.resolveAccount(rt, params.To)
builtin.RequireNoErr(rt, err, exitcode.Unwrap(err, exitcode.ErrIllegalState), "failed to resolve to address: %s", params.To)
from, err := pca.resolveAccount(rt, params.From)
builtin.RequireNoErr(rt, err, exitcode.Unwrap(err, exitcode.ErrIllegalState), "failed to resolve from address: %s", params.From)
emptyArr, err := adt.MakeEmptyArray(adt.AsStore(rt), LaneStatesAmtBitwidth)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to create empty array")
emptyArrCid, err := emptyArr.Root()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to persist empty array")
st := ConstructState(from, to, emptyArrCid)
rt.StateCreate(st)
return nil
}
// Resolves an address to a canonical ID address and requires it to address an account actor.
func (pca *Actor) resolveAccount(rt runtime.Runtime, raw addr.Address) (addr.Address, error) {
resolved, err := builtin.ResolveToIDAddr(rt, raw)
if err != nil {
return addr.Undef, exitcode.ErrIllegalState.Wrapf("failed to resolve address %v: %w", raw, err)
}
codeCID, ok := rt.GetActorCodeCID(resolved)
if !ok {
return addr.Undef, exitcode.ErrIllegalArgument.Wrapf("no code for address %v", resolved)
}
if codeCID != builtin.AccountActorCodeID {
return addr.Undef, exitcode.ErrForbidden.Wrapf("actor %v must be an account (%v), was %v", raw,
builtin.AccountActorCodeID, codeCID)
}
return resolved, nil
}
////////////////////////////////////////////////////////////////////////////////
// Payment Channel state operations
////////////////////////////////////////////////////////////////////////////////
type UpdateChannelStateParams = paych7.UpdateChannelStateParams
type SignedVoucher = paych7.SignedVoucher
func VoucherSigningBytes(t *SignedVoucher) ([]byte, error) {
osv := *t
osv.Signature = nil
buf := new(bytes.Buffer)
if err := osv.MarshalCBOR(buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Modular Verification method
//type ModVerifyParams struct {
// // Actor on which to invoke the method.
// Actor addr.Address
// // Method to invoke.
// Method abi.MethodNum
// // Pre-serialized method parameters.
// Params []byte
//}
type ModVerifyParams = paych0.ModVerifyParams
// Specifies which `Lane`s to be merged with what `Nonce` on channelUpdate
//type Merge struct {
// Lane uint64
// Nonce uint64
//}
type Merge = paych0.Merge
func (pca Actor) UpdateChannelState(rt runtime.Runtime, params *UpdateChannelStateParams) *abi.EmptyValue {
var st State
rt.StateReadonly(&st)
// both parties must sign voucher: one who submits it, the other explicitly signs it
rt.ValidateImmediateCallerIs(st.From, st.To)
var signer addr.Address
if rt.Caller() == st.From {
signer = st.To
} else {
signer = st.From
}
sv := params.Sv
if sv.Signature == nil {
rt.Abortf(exitcode.ErrIllegalArgument, "voucher has no signature")
}
if st.SettlingAt != 0 && rt.CurrEpoch() >= st.SettlingAt {
rt.Abortf(ErrChannelStateUpdateAfterSettled, "no vouchers can be processed after SettlingAt epoch")
}
if len(params.Secret) > MaxSecretSize {
rt.Abortf(exitcode.ErrIllegalArgument, "secret must be at most 256 bytes long")
}
vb, err := VoucherSigningBytes(&sv)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalArgument, "failed to serialize signedvoucher")
err = rt.VerifySignature(*sv.Signature, signer, vb)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalArgument, "voucher signature invalid")
pchAddr := rt.Receiver()
svpchIDAddr, found := rt.ResolveAddress(sv.ChannelAddr)
if !found {
rt.Abortf(exitcode.ErrIllegalArgument, "voucher payment channel address %s does not resolve to an ID address", sv.ChannelAddr)
}
if pchAddr != svpchIDAddr {
rt.Abortf(exitcode.ErrIllegalArgument, "voucher payment channel address %s does not match receiver %s", svpchIDAddr, pchAddr)
}
if rt.CurrEpoch() < sv.TimeLockMin {
rt.Abortf(exitcode.ErrIllegalArgument, "cannot use this voucher yet!")
}
if sv.TimeLockMax != 0 && rt.CurrEpoch() > sv.TimeLockMax {
rt.Abortf(exitcode.ErrIllegalArgument, "this voucher has expired!")
}
if sv.Amount.Sign() < 0 {
rt.Abortf(exitcode.ErrIllegalArgument, "voucher amount must be non-negative, was %v", sv.Amount)
}
if len(sv.SecretHash) > 0 {
hashedSecret := rt.HashBlake2b(params.Secret)
if !bytes.Equal(hashedSecret[:], sv.SecretHash) {
rt.Abortf(exitcode.ErrIllegalArgument, "incorrect secret!")
}
}
if sv.Extra != nil {
code := rt.Send(
sv.Extra.Actor,
sv.Extra.Method,
builtin.CBORBytes(sv.Extra.Data),
abi.NewTokenAmount(0),
&builtin.Discard{},
)
builtin.RequireSuccess(rt, code, "spend voucher verification failed")
}
rt.StateTransaction(&st, func() {
laneFound := true
lstates, err := adt.AsArray(adt.AsStore(rt), st.LaneStates, LaneStatesAmtBitwidth)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load lanes")
// Find the voucher lane, creating if necessary.
laneId := sv.Lane
laneState := findLane(rt, lstates, sv.Lane)
if laneState == nil {
laneState = &LaneState{
Redeemed: big.Zero(),
Nonce: 0,
}
laneFound = false
}
if laneFound {
if laneState.Nonce >= sv.Nonce {
rt.Abortf(exitcode.ErrIllegalArgument, "voucher has an outdated nonce, existing nonce: %d, voucher nonce: %d, cannot redeem",
laneState.Nonce, sv.Nonce)
}
}
// The next section actually calculates the payment amounts to update the payment channel state
// 1. (optional) sum already redeemed value of all merging lanes
redeemedFromOthers := big.Zero()
for _, merge := range sv.Merges {
if merge.Lane == sv.Lane {
rt.Abortf(exitcode.ErrIllegalArgument, "voucher cannot merge lanes into its own lane")
}
otherls := findLane(rt, lstates, merge.Lane)
if otherls == nil {
rt.Abortf(exitcode.ErrIllegalArgument, "voucher specifies invalid merge lane %v", merge.Lane)
return // makes linters happy
}
if otherls.Nonce >= merge.Nonce {
rt.Abortf(exitcode.ErrIllegalArgument, "merged lane in voucher has outdated nonce, cannot redeem")
}
redeemedFromOthers = big.Add(redeemedFromOthers, otherls.Redeemed)
otherls.Nonce = merge.Nonce
err = lstates.Set(merge.Lane, otherls)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to store lane %d", merge.Lane)
}
// 2. To prevent double counting, remove already redeemed amounts (from
// voucher or other lanes) from the voucher amount
laneState.Nonce = sv.Nonce
balanceDelta := big.Sub(sv.Amount, big.Add(redeemedFromOthers, laneState.Redeemed))
// 3. set new redeemed value for merged-into lane
laneState.Redeemed = sv.Amount
newSendBalance := big.Add(st.ToSend, balanceDelta)
// 4. check operation validity
if newSendBalance.LessThan(big.Zero()) {
rt.Abortf(exitcode.ErrIllegalArgument, "voucher would leave channel balance negative")
}
if newSendBalance.GreaterThan(rt.CurrentBalance()) {
rt.Abortf(exitcode.ErrIllegalArgument, "not enough funds in channel to cover voucher")
}
// 5. add new redemption ToSend
st.ToSend = newSendBalance
// update channel settlingAt and MinSettleHeight if delayed by voucher
if sv.MinSettleHeight != 0 {
if st.SettlingAt != 0 && st.SettlingAt < sv.MinSettleHeight {
st.SettlingAt = sv.MinSettleHeight
}
if st.MinSettleHeight < sv.MinSettleHeight {
st.MinSettleHeight = sv.MinSettleHeight
}
}
err = lstates.Set(laneId, laneState)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to store lane", laneId)
st.LaneStates, err = lstates.Root()
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to save lanes")
})
return nil
}
func (pca Actor) Settle(rt runtime.Runtime, _ *abi.EmptyValue) *abi.EmptyValue {
var st State
rt.StateTransaction(&st, func() {
rt.ValidateImmediateCallerIs(st.From, st.To)
if st.SettlingAt != 0 {
rt.Abortf(exitcode.ErrIllegalState, "channel already settling")
}
st.SettlingAt = rt.CurrEpoch() + SettleDelay
if st.SettlingAt < st.MinSettleHeight {
st.SettlingAt = st.MinSettleHeight
}
})
return nil
}
func (pca Actor) Collect(rt runtime.Runtime, _ *abi.EmptyValue) *abi.EmptyValue {
var st State
rt.StateReadonly(&st)
rt.ValidateImmediateCallerIs(st.From, st.To)
if st.SettlingAt == 0 || rt.CurrEpoch() < st.SettlingAt {
rt.Abortf(exitcode.ErrForbidden, "payment channel not settling or settled")
}
// send ToSend to "To"
codeTo := rt.Send(
st.To,
builtin.MethodSend,
nil,
st.ToSend,
&builtin.Discard{},
)
builtin.RequireSuccess(rt, codeTo, "Failed to send funds to `To`")
// the remaining balance will be returned to "From" upon deletion.
rt.DeleteActor(st.From)
return nil
}
// Returns the insertion index for a lane ID, with the matching lane state if found, or nil.
func findLane(rt runtime.Runtime, ls *adt.Array, id uint64) *LaneState {
if id > MaxLane {
rt.Abortf(exitcode.ErrIllegalArgument, "maximum lane ID is 2^63-1")
}
var out LaneState
found, err := ls.Get(id, &out)
builtin.RequireNoErr(rt, err, exitcode.ErrIllegalState, "failed to load lane %d", id)
if !found {
return nil
}
return &out
}
From
toTo
off-chain in order to enableTo
to redeem payments on-chain in the future