Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Locking channel accounts to transactions #116

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

gouthamp-stellar
Copy link
Contributor

@gouthamp-stellar gouthamp-stellar commented Jan 26, 2025

What

Instead of locking channel accounts for an arbitrary amount of time like we are currently doing, lock them for the same amount of time as the transaction time bounds and also explicitly lock them to transactions (when the transaction is being built), opportunistically unlocking them from their transactions in the webhook channel

Why

If the channel account is specified as the source of a transaction that hasn't been accepted by the network yet, and the same channel account is "unlocked" and then used as the source for a different transaction that gets accepted by the network prior to the original, the original transaction is invalidated. We want to avoid this behavior

Known limitations

N/A

Issue that this PR addresses

https://github.com/orgs/stellar/projects/58/views/2?pane=issue&itemId=92871835&issue=stellar%7Cwallet-backend%7C102

Checklist

PR Structure

  • It is not possible to break this PR down into smaller PRs.
  • This PR does not mix refactoring changes with feature changes.
  • This PR's title starts with name of package that is most changed in the PR, or all if the changes are broad or impact many packages.

Thoroughness

  • This PR adds tests for the new functionality or fixes.
  • All updated queries have been tested (refer to this check if the data set returned by the updated query is expected to be same as the original one).

Release

  • This is not a breaking change.
  • This is ready to be tested in development.
  • The new functionality is gated with a feature flag if this is not ready for production.

Instead of locking channel accounts for an arbitrary amount of time, lock them for the same amount of time as the transaction time bounds and also explicitly lock them to transactions, unlocking them in the webhook channel
Comment on lines 46 to 55
func (sc *channelAccountDBSignatureClient) GetAccountPublicKey(ctx context.Context, opts ...int) (string, error) {
var lockedUntil time.Duration
if len(opts) > 0 {
lockedUntil = time.Duration(opts[0]) * time.Second
} else {
lockedUntil = time.Minute
}
for range store.ChannelAccountWaitTime {
channelAccount, err := sc.channelAccountStore.GetIdleChannelAccount(ctx, time.Minute)
// check to see if the variadic parameter for time exists and if so, use it here
channelAccount, err := sc.channelAccountStore.GetIdleChannelAccount(ctx, lockedUntil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this function accept multiple integers? I only see the first one used

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to pass in the time for which to lock a channel account as an optional argument, so that not every signature client has to pass it in, which is why I made it a variadic function

@@ -29,20 +29,20 @@ func (ca *ChannelAccountModel) GetIdleChannelAccount(ctx context.Context, locked
query := fmt.Sprintf(`
UPDATE channel_accounts
SET
locked_tx_hash = NULL,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is unnecessary since UnlockChannelAccountFromTx() sets this to null

Copy link
Contributor Author

@gouthamp-stellar gouthamp-stellar Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is true in theory, but in case the channel accounts do not get unlocked (they fail to reach the webhook for example), this will act as a fail safe, which is why I added it in there

internal/signing/store/channel_accounts_model.go Outdated Show resolved Hide resolved
@JakeUrban
Copy link
Contributor

resolves #102

@@ -36,7 +36,7 @@ func (sc *envSignatureClient) NetworkPassphrase() string {
return sc.networkPassphrase
}

func (sc *envSignatureClient) GetAccountPublicKey(ctx context.Context) (string, error) {
func (sc *envSignatureClient) GetAccountPublicKey(ctx context.Context, opts ...int) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (sc *envSignatureClient) GetAccountPublicKey(ctx context.Context, opts ...int) (string, error) {
func (sc *envSignatureClient) GetAccountPublicKey(ctx context.Context, _ ...int) (string, error) {

@@ -59,7 +59,7 @@ func NewKMSSignatureClient(publicKey string, networkPassphrase string, keypairSt
}, nil
}

func (sc *kmsSignatureClient) GetAccountPublicKey(ctx context.Context) (string, error) {
func (sc *kmsSignatureClient) GetAccountPublicKey(ctx context.Context, opts ...int) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (sc *kmsSignatureClient) GetAccountPublicKey(ctx context.Context, opts ...int) (string, error) {
func (sc *kmsSignatureClient) GetAccountPublicKey(ctx context.Context, _ ...int) (string, error) {

@@ -18,7 +18,7 @@ func (s *SignatureClientMock) NetworkPassphrase() string {
return args.String(0)
}

func (s *SignatureClientMock) GetAccountPublicKey(ctx context.Context) (string, error) {
func (s *SignatureClientMock) GetAccountPublicKey(ctx context.Context, opts ...int) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (s *SignatureClientMock) GetAccountPublicKey(ctx context.Context, opts ...int) (string, error) {
func (s *SignatureClientMock) GetAccountPublicKey(ctx context.Context, _ ...int) (string, error) {

@@ -69,7 +77,7 @@ func (t *transactionService) NetworkPassphrase() string {
}

func (t *transactionService) BuildAndSignTransactionWithChannelAccount(ctx context.Context, operations []txnbuild.Operation, timeoutInSecs int64) (*txnbuild.Transaction, error) {
channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(ctx)
channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(ctx, int(timeoutInSecs))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I am wrong but I think there is a bug here: what happens if we get an error in the code until we assign the locked channel account to the txn? In this case the function exits and the db change to lock channel account is not rolled back, sinceExecContext executes a single SQL statement - any error will leave the database in its current state. The channel account will be unavailable until the timeout expires.

A better design would be to start a db transaction and do calls to GetAndLockIdleChannelAccount and AssignTxToChannelAccount in the same txn and rollback the txn if the function exits due to an error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants