Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions pkg/settlement/pseudosettle/pseudosettle.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ type Service struct {
}

type pseudoSettlePeer struct {
lock sync.Mutex // lock to be held during receiving a payment from this peer
fullNode bool
lock sync.Mutex // lock to be held during receiving a payment from this peer
fullNode bool
connectedAt int64 // unix time the peer connected; anchors the first-contact allowance
}

type lastPayment struct {
Expand Down Expand Up @@ -108,7 +109,7 @@ func (s *Service) init(ctx context.Context, p p2p.Peer) error {

_, ok := s.peers[p.Address.String()]
if !ok {
peerData := &pseudoSettlePeer{fullNode: p.FullNode}
peerData := &pseudoSettlePeer{fullNode: p.FullNode, connectedAt: s.timeNow().Unix()}
s.peers[p.Address.String()] = peerData
}

Expand Down Expand Up @@ -142,14 +143,18 @@ func totalKeyPeer(key []byte, prefix string) (peer swarm.Address, err error) {

// peerAllowance computes the maximum incoming payment value we accept
// this is the time based allowance or the peers actual debt, whichever is less
func (s *Service) peerAllowance(peer swarm.Address, fullNode bool) (limit *big.Int, stamp int64, err error) {
func (s *Service) peerAllowance(peer swarm.Address, fullNode bool, connectedAt int64) (limit *big.Int, stamp int64, err error) {
var lastTime lastPayment
err = s.store.Get(totalKey(peer, SettlementReceivedPrefix), &lastTime)
if err != nil {
if !errors.Is(err, storage.ErrNotFound) {
return nil, 0, err
}
lastTime.Timestamp = int64(0)
// First contact with this peer: anchor the time-based allowance to the
// connection time rather than the zero epoch. Anchoring at zero would
// scale the entire Unix epoch by the refresh rate and let the initial
// refreshment forgive an effectively unbounded debt.
lastTime.Timestamp = connectedAt
}

currentTime := s.timeNow().Unix()
Expand Down Expand Up @@ -210,7 +215,7 @@ func (s *Service) handler(ctx context.Context, p p2p.Peer, stream p2p.Stream) (e
pseudoSettlePeer.lock.Lock()
defer pseudoSettlePeer.lock.Unlock()

allowance, timestamp, err := s.peerAllowance(p.Address, pseudoSettlePeer.fullNode)
allowance, timestamp, err := s.peerAllowance(p.Address, pseudoSettlePeer.fullNode, pseudoSettlePeer.connectedAt)
if err != nil {
return err
}
Expand Down
53 changes: 53 additions & 0 deletions pkg/settlement/pseudosettle/pseudosettle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,9 @@ func TestPayment(t *testing.T) {
receiverObserver := newTestObserver(map[string]*big.Int{peerID.String(): big.NewInt(debt)}, map[string]*big.Int{})
recipient := pseudosettle.New(nil, logger, storeRecipient, receiverObserver, big.NewInt(testRefreshRate), big.NewInt(testRefreshRateLight), mockp2p.New())
recipient.SetAccounting(receiverObserver)
// Establish a connection time before init so the first-contact allowance is
// anchored to it rather than to the Unix epoch.
recipient.SetTime(1)
err := recipient.Init(context.Background(), peer)
if err != nil {
t.Fatal(err)
Expand All @@ -295,6 +298,50 @@ func TestPayment(t *testing.T) {
testCaseAccepted(t, recorder, payerObserver, receiverObserver, payer, recipient, peerID, 30, 30, 1, 1, 1, big.NewInt(debt), big.NewInt(debt), big.NewInt(debt), big.NewInt(debt))
}

// TestFirstContactAllowanceBounded verifies that the time-based allowance granted
// on first contact is bounded by the time elapsed since the peer connected, not by
// the Unix epoch. A peer connected one second ago may only settle a single refresh
// interval of debt, no matter how large its outstanding debt is. Anchoring at the
// epoch would let the first refreshment forgive an effectively unbounded amount.
func TestFirstContactAllowanceBounded(t *testing.T) {
t.Parallel()

logger := log.Noop

storeRecipient := newStateStore(t)

peerID := swarm.MustParseHexAddress("9ee7add7")
peer := p2p.Peer{Address: peerID, FullNode: true}

// Debt far larger than a single refresh interval's worth of allowance.
debt := int64(1000) * testRefreshRate

payerObserver := newTestObserver(map[string]*big.Int{peerID.String(): big.NewInt(debt)}, map[string]*big.Int{})
receiverObserver := newTestObserver(map[string]*big.Int{peerID.String(): big.NewInt(debt)}, map[string]*big.Int{})
recipient := pseudosettle.New(nil, logger, storeRecipient, receiverObserver, big.NewInt(testRefreshRate), big.NewInt(testRefreshRateLight), mockp2p.New())
recipient.SetAccounting(receiverObserver)
// The peer connects at t=1000000.
recipient.SetTime(1000000)
err := recipient.Init(context.Background(), peer)
if err != nil {
t.Fatal(err)
}

recorder := streamtest.New(
streamtest.WithProtocols(recipient.Protocol()),
streamtest.WithBaseAddr(peerID),
)

storePayer := newStateStore(t)

payer := pseudosettle.New(recorder, logger, storePayer, payerObserver, big.NewInt(testRefreshRate), big.NewInt(testRefreshRateLight), mockp2p.New())
payer.SetAccounting(payerObserver)

// One second after connecting the peer attempts to settle its entire debt.
// Only one refresh interval (testRefreshRate) may be accepted, not the full debt.
testCaseAccepted(t, recorder, payerObserver, receiverObserver, payer, recipient, peerID, 1000001, 1000001, 1, 1, 1, big.NewInt(debt), big.NewInt(debt), big.NewInt(testRefreshRate), big.NewInt(testRefreshRate))
}

func TestTimeLimitedPayment(t *testing.T) {
t.Parallel()

Expand All @@ -311,6 +358,9 @@ func TestTimeLimitedPayment(t *testing.T) {
receiverObserver := newTestObserver(map[string]*big.Int{peerID.String(): big.NewInt(debt)}, map[string]*big.Int{})
recipient := pseudosettle.New(nil, logger, storeRecipient, receiverObserver, big.NewInt(testRefreshRate), big.NewInt(testRefreshRateLight), mockp2p.New())
recipient.SetAccounting(receiverObserver)
// Establish a connection time before init so the first-contact allowance is
// anchored to it rather than to the Unix epoch.
recipient.SetTime(1)
err := recipient.Init(context.Background(), peer)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -365,6 +415,9 @@ func TestTimeLimitedPaymentLight(t *testing.T) {
receiverObserver := newTestObserver(map[string]*big.Int{peerID.String(): big.NewInt(debt)}, map[string]*big.Int{})
recipient := pseudosettle.New(nil, logger, storeRecipient, receiverObserver, big.NewInt(testRefreshRate), big.NewInt(testRefreshRateLight), mockp2p.New())
recipient.SetAccounting(receiverObserver)
// Establish a connection time before init so the first-contact allowance is
// anchored to it rather than to the Unix epoch.
recipient.SetTime(1)
err := recipient.Init(context.Background(), peer)
if err != nil {
t.Fatal(err)
Expand Down
Loading