Skip to content
This repository has been archived by the owner on Apr 23, 2024. It is now read-only.

Commit

Permalink
sdk/state, sdk/agent: add memos to payments (#348)
Browse files Browse the repository at this point in the history
Add a memo field to close agreements that is optional. The memo is a string with unspecified bounds. The memo is not included in the final transaction so its contents does not leak to the public ledger when the channel is closed. Subsequently the signatures for close agreements to do not cover the memo.

Payments should have memos so they can be identifiable on the terms of the participants. Participants may communicate about a payment ahead of sending a payment and the memo will provide a way to annotate or identify the payment in whatever way they require.

Note that this is similar but different to #339. #339 is concerned with uniquely identifying payments with an identifier that the state machine attaches to close agreements. That would not be useful in the situation described above where an identifier must be assigned prior to the payment being made.

This is useful for things like buffered channels, something I'm experimenting with right now for #340.
  • Loading branch information
leighmcculloch authored Sep 30, 2021
1 parent 4c828b6 commit 577a257
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 5 deletions.
11 changes: 10 additions & 1 deletion sdk/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,15 @@ func (a *Agent) Open() error {
// The payment is not authorized until the remote participant signs the payment
// and returns the payment.
func (a *Agent) Payment(paymentAmount int64) error {
return a.PaymentWithMemo(paymentAmount, "")
}

// Payment makes a payment of the payment amount to the remote participant using
// the open channel. The process is asynchronous and the function returns
// immediately after the payment is signed and sent to the remote participant.
// The payment is not authorized until the remote participant signs the payment
// and returns the payment. The memo is attached to the payment.
func (a *Agent) PaymentWithMemo(paymentAmount int64, memo string) error {
a.mu.Lock()
defer a.mu.Unlock()

Expand All @@ -288,7 +297,7 @@ func (a *Agent) Payment(paymentAmount int64) error {

defer a.snapshot()

ca, err := a.channel.ProposePayment(paymentAmount)
ca, err := a.channel.ProposePaymentWithMemo(paymentAmount, memo)
if errors.Is(err, state.ErrUnderfunded) {
fmt.Fprintf(a.logWriter, "local is underfunded for this payment based on cached account balances, checking escrow account...\n")
var balance int64
Expand Down
19 changes: 15 additions & 4 deletions sdk/state/payment.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,25 @@ type CloseDetails struct {
ObservationPeriodLedgerGap int64
IterationNumber int64
Balance int64
PaymentAmount int64
ProposingSigner *keypair.FromAddress
ConfirmingSigner *keypair.FromAddress

// The following fields are not captured in the signatures produced by
// signers because the information is not embedded into the agreement's
// transactions.
PaymentAmount int64
Memo string
}

func (d CloseDetails) Equal(d2 CloseDetails) bool {
return d.ObservationPeriodTime == d2.ObservationPeriodTime &&
d.ObservationPeriodLedgerGap == d2.ObservationPeriodLedgerGap &&
d.IterationNumber == d2.IterationNumber &&
d.Balance == d2.Balance &&
d.PaymentAmount == d2.PaymentAmount &&
d.ProposingSigner.Equal(d2.ProposingSigner) &&
d.ConfirmingSigner.Equal(d2.ConfirmingSigner)
d.ConfirmingSigner.Equal(d2.ConfirmingSigner) &&
d.PaymentAmount == d2.PaymentAmount &&
d.Memo == d2.Memo
}

type CloseSignatures struct {
Expand Down Expand Up @@ -147,6 +153,10 @@ func (ca CloseAgreement) SignedTransactions() CloseTransactions {
}

func (c *Channel) ProposePayment(amount int64) (CloseAgreement, error) {
return c.ProposePaymentWithMemo(amount, "")
}

func (c *Channel) ProposePaymentWithMemo(amount int64, memo string) (CloseAgreement, error) {
if amount < 0 {
return CloseAgreement{}, fmt.Errorf("payment amount must not be less than 0")
}
Expand Down Expand Up @@ -189,9 +199,10 @@ func (c *Channel) ProposePayment(amount int64) (CloseAgreement, error) {
ObservationPeriodLedgerGap: c.latestAuthorizedCloseAgreement.Envelope.Details.ObservationPeriodLedgerGap,
IterationNumber: c.NextIterationNumber(),
Balance: newBalance,
PaymentAmount: amount,
ProposingSigner: c.localSigner.FromAddress(),
ConfirmingSigner: c.remoteSigner,
PaymentAmount: amount,
Memo: memo,
}
txs, err := c.closeTxs(c.openAgreement.Envelope.Details, d)
if err != nil {
Expand Down
85 changes: 85 additions & 0 deletions sdk/state/payment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,91 @@ func TestChannel_ProposeAndConfirmPayment_allowZeroAmountPayment(t *testing.T) {
require.NoError(t, err)
}

func TestChannel_ProposeAndConfirmPayment_withMemo(t *testing.T) {
localSigner := keypair.MustRandom()
remoteSigner := keypair.MustRandom()
localEscrowAccount := keypair.MustRandom().FromAddress()
remoteEscrowAccount := keypair.MustRandom().FromAddress()

// Given a channel with observation periods set to 1.
responderChannel := NewChannel(Config{
NetworkPassphrase: network.TestNetworkPassphrase,
Initiator: false,
LocalSigner: localSigner,
RemoteSigner: remoteSigner.FromAddress(),
LocalEscrowAccount: localEscrowAccount,
RemoteEscrowAccount: remoteEscrowAccount,
MaxOpenExpiry: 2 * time.Hour,
})
initiatorChannel := NewChannel(Config{
NetworkPassphrase: network.TestNetworkPassphrase,
Initiator: true,
LocalSigner: remoteSigner,
RemoteSigner: localSigner.FromAddress(),
LocalEscrowAccount: remoteEscrowAccount,
RemoteEscrowAccount: localEscrowAccount,
MaxOpenExpiry: 2 * time.Hour,
})

// Put channel into the Open state.
{
m, err := initiatorChannel.ProposeOpen(OpenParams{
ObservationPeriodLedgerGap: 1,
Asset: NativeAsset,
ExpiresAt: time.Now().Add(5 * time.Minute),
StartingSequence: 101,
})
require.NoError(t, err)
m, err = responderChannel.ConfirmOpen(m.Envelope)
require.NoError(t, err)
_, err = initiatorChannel.ConfirmOpen(m.Envelope)
require.NoError(t, err)

ftx, err := initiatorChannel.OpenTx()
require.NoError(t, err)
ftxXDR, err := ftx.Base64()
require.NoError(t, err)

successResultXDR, err := txbuildtest.BuildResultXDR(true)
require.NoError(t, err)
resultMetaXDR, err := txbuildtest.BuildFormationResultMetaXDR(txbuildtest.FormationResultMetaParams{
InitiatorSigner: remoteSigner.Address(),
ResponderSigner: localSigner.Address(),
InitiatorEscrow: remoteEscrowAccount.Address(),
ResponderEscrow: localEscrowAccount.Address(),
StartSequence: 101,
Asset: txnbuild.NativeAsset{},
})
require.NoError(t, err)

err = initiatorChannel.IngestTx(1, ftxXDR, successResultXDR, resultMetaXDR)
require.NoError(t, err)

cs, err := initiatorChannel.State()
require.NoError(t, err)
assert.Equal(t, StateOpen, cs)

err = responderChannel.IngestTx(1, ftxXDR, successResultXDR, resultMetaXDR)
require.NoError(t, err)

cs, err = responderChannel.State()
require.NoError(t, err)
assert.Equal(t, StateOpen, cs)
}

initiatorChannel.UpdateLocalEscrowAccountBalance(100)
responderChannel.UpdateRemoteEscrowAccountBalance(100)

ca, err := initiatorChannel.ProposePaymentWithMemo(1, "id1")
require.NoError(t, err)
assert.Equal(t, "id1", ca.Envelope.Details.Memo)
caResponse, err := responderChannel.ConfirmPayment(ca.Envelope)
require.NoError(t, err)
assert.Equal(t, "id1", caResponse.Envelope.Details.Memo)
_, err = initiatorChannel.ConfirmPayment(caResponse.Envelope)
require.NoError(t, err)
}

func TestChannel_ConfirmPayment_signatureChecks(t *testing.T) {
localSigner := keypair.MustRandom()
remoteSigner := keypair.MustRandom()
Expand Down

0 comments on commit 577a257

Please sign in to comment.