-
Notifications
You must be signed in to change notification settings - Fork 3
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
base: main
Are you sure you want to change the base?
Conversation
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
internal/db/migrations/2025-01-22.0-add_locked_tx_hash_to_channel_accounts.sql
Outdated
Show resolved
Hide resolved
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)) |
There was a problem hiding this comment.
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.
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
all
if the changes are broad or impact many packages.Thoroughness
Release