Skip to content

Commit 1452069

Browse files
committed
solana: refactor conflict checking and introduce SetPayload for call inputs
1 parent 0e9ff9e commit 1452069

File tree

6 files changed

+148
-60
lines changed

6 files changed

+148
-60
lines changed

chain/evm/call/call.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,7 @@ func (tx *TxCall) Serialize() ([]byte, error) {
144144
}
145145
return ethTx.MarshalBinary()
146146
}
147+
148+
func (tx *TxCall) GetPayload() (xc.TxCallPayload, bool) {
149+
return nil, false
150+
}

chain/solana/call/call.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func (c *TxCall) IsRetryable() (bool, string) {
132132
}
133133

134134
func (c *TxCall) SetInput(input xc.CallTxInput) error {
135-
txInput := input.(*tx_input.CallInput)
135+
txInput := input.(tx_input.GetTxInfo)
136136

137137
if ok, _ := c.IsRetryable(); !ok {
138138
// we cannot modify the transaction in any way as it has an external signer.
@@ -141,18 +141,11 @@ func (c *TxCall) SetInput(input xc.CallTxInput) error {
141141

142142
// If the nonce account is used, do not use recent-blockhash, as this
143143
// transaction is very likely using durable-nonce.
144-
usingDurableNonce := false
145-
for _, accountKey := range c.SolTx.Message.AccountKeys {
146-
if !txInput.DurableNonceAccount.IsZero() && txInput.DurableNonceAccount.Equals(accountKey) {
147-
usingDurableNonce = true
148-
break
149-
}
150-
151-
}
144+
usingDurableNonce := txInput.DoesTxUseDurableNonce(c.SolTx)
152145
if !usingDurableNonce {
153146
// Update using the latest blockhash to prevent unwanted expiry.
154147
// Otherwise it's bad experience if transaction was waiting for approval only to immediately expire after.
155-
c.SolTx.Message.RecentBlockhash = txInput.RecentBlockHash
148+
c.SolTx.Message.RecentBlockhash = txInput.GetRecentBlockhash()
156149
}
157150

158151
return nil
@@ -220,3 +213,7 @@ func (c *TxCall) SetSignatures(signatures ...*xc.SignatureResponse) error {
220213
func (c *TxCall) Serialize() ([]byte, error) {
221214
return c.SolTx.MarshalBinary()
222215
}
216+
217+
func (c *TxCall) GetPayload() (xc.TxCallPayload, bool) {
218+
return tx_input.NewCallPayload(c.SolTx), true
219+
}

chain/solana/tx_input/call.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package tx_input
22

33
import (
4+
"fmt"
5+
46
xc "github.com/cordialsys/crosschain"
57
"github.com/cordialsys/crosschain/factory/drivers/registry"
8+
"github.com/gagliardetto/solana-go"
69
)
710

811
func init() {
@@ -13,6 +16,8 @@ type CallInput struct {
1316
TxInput
1417
}
1518

19+
var _ xc.TxInput = &CallInput{}
20+
var _ xc.TxInputWithCall = &CallInput{}
1621
var _ xc.TxVariantInput = &CallInput{}
1722
var _ xc.CallTxInput = &CallInput{}
1823

@@ -21,3 +26,32 @@ func (*CallInput) Calling() {}
2126
func (*CallInput) GetVariant() xc.TxVariantInputType {
2227
return xc.NewCallingInputType(xc.DriverSolana)
2328
}
29+
30+
func NewCallPayload(solTx *solana.Transaction) *CallPayload {
31+
return &CallPayload{solTx}
32+
}
33+
34+
type CallPayload struct {
35+
solTx *solana.Transaction
36+
}
37+
38+
var _ xc.TxCallPayload = &CallPayload{}
39+
40+
func (p *CallPayload) IsTxCallPayload() {}
41+
42+
func (input *CallInput) SetCall(call xc.TxCallPayload) error {
43+
txCall, ok := call.(*CallPayload)
44+
if !ok {
45+
return fmt.Errorf("invalid call payload for solana: %T", call)
46+
}
47+
48+
solanaTx := txCall.solTx
49+
input.TxInput.RecentBlockHash = solanaTx.Message.RecentBlockhash
50+
if input.DoesTxUseDurableNonce(solanaTx) {
51+
input.TxInput.DurableNonce = solanaTx.Message.RecentBlockhash
52+
} else {
53+
input.TxInput.DurableNonceAccount = solana.PublicKey{}
54+
input.TxInput.DurableNonce = solana.Hash{}
55+
}
56+
return nil
57+
}

chain/solana/tx_input/tx_input.go

Lines changed: 72 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,13 @@ type TxInput struct {
3737
type GetTxInfo interface {
3838
GetTimestamp() int64
3939
GetRecentBlockhash() solana.Hash
40-
}
4140

42-
// GetDurableNonceInfo is an interface to retrieve durable nonce information from a TxInput.
43-
type GetDurableNonceInfo interface {
4441
GetDurableNonceAccount() solana.PublicKey
4542
GetDurableNonceValue() solana.Hash
4643
HasDurableNonce() bool
4744
IsCreatingDurableNonceAccount() bool
45+
46+
DoesTxUseDurableNonce(tx *solana.Transaction) bool
4847
}
4948

5049
type TokenAccount struct {
@@ -54,7 +53,6 @@ type TokenAccount struct {
5453

5554
var _ xc.TxInput = &TxInput{}
5655
var _ GetTxInfo = &TxInput{}
57-
var _ GetDurableNonceInfo = &TxInput{}
5856
var _ xc.TxInputWithUnix = &TxInput{}
5957

6058
func init() {
@@ -90,6 +88,25 @@ func (input *TxInput) IsCreatingDurableNonceAccount() bool {
9088
return input.ShouldCreateDurableNonce && !input.DurableNonceAccount.IsZero()
9189
}
9290

91+
func (input *TxInput) DoesTxUseDurableNonce(tx *solana.Transaction) bool {
92+
if !input.HasDurableNonce() {
93+
return false
94+
}
95+
96+
if tx.Message.RecentBlockhash.Equals(input.DurableNonce) {
97+
return true
98+
}
99+
usingDurableNonce := false
100+
for _, accountKey := range tx.Message.AccountKeys {
101+
if input.DurableNonceAccount.Equals(accountKey) {
102+
usingDurableNonce = true
103+
break
104+
}
105+
}
106+
107+
return usingDurableNonce
108+
}
109+
93110
// GetBlockhashForTx returns the blockhash to use for the transaction.
94111
// If a durable nonce is set and initialized, the nonce value is used instead of a recent blockhash.
95112
// When the nonce account needs to be created first, the recent blockhash is used.
@@ -155,50 +172,71 @@ func (input *TxInput) IsFeeLimitAccurate() bool {
155172
return true
156173
}
157174

175+
func (input *TxInput) MatchDurableNonce(otherNonce GetTxInfo) (accountMatch, nonceMatch bool) {
176+
sameAccount := input.DurableNonceAccount.Equals(otherNonce.GetDurableNonceAccount())
177+
if sameAccount {
178+
// Both creating the same nonce account = conflict
179+
if input.IsCreatingDurableNonceAccount() && otherNonce.IsCreatingDurableNonceAccount() {
180+
return true, true
181+
}
182+
// Both using the same nonce value = conflict (only one can succeed)
183+
// Different nonce values = independent (each uses its own nonce)
184+
if input.HasDurableNonce() && otherNonce.HasDurableNonce() {
185+
return true, input.DurableNonce.Equals(otherNonce.GetDurableNonceValue())
186+
}
187+
}
188+
// different accounts = independent
189+
return false, false
190+
}
191+
192+
func (input *TxInput) DidTimeoutOccur(other GetTxInfo) (timeout bool) {
193+
diff := input.Timestamp - other.GetTimestamp()
194+
if diff < int64(SafetyTimeoutMargin.Seconds()) || other.GetRecentBlockhash().Equals(input.GetRecentBlockhash()) {
195+
return false
196+
}
197+
return true
198+
}
199+
158200
func (input *TxInput) IndependentOf(other xc.TxInput) (independent bool) {
159-
if otherNonce, ok := other.(GetDurableNonceInfo); ok {
160-
sameAccount := !input.DurableNonceAccount.IsZero() &&
161-
input.DurableNonceAccount.Equals(otherNonce.GetDurableNonceAccount())
162-
if sameAccount {
163-
// Both creating the same nonce account = conflict
164-
if input.IsCreatingDurableNonceAccount() && otherNonce.IsCreatingDurableNonceAccount() {
165-
return false
166-
}
167-
// Both using the same nonce value = conflict (only one can succeed)
168-
// Different nonce values = independent (each uses its own nonce)
169-
if input.HasDurableNonce() && otherNonce.HasDurableNonce() {
170-
return !input.DurableNonce.Equals(otherNonce.GetDurableNonceValue())
171-
}
201+
if otherNonce, ok := other.(GetTxInfo); ok {
202+
// if input.HasDurableNonce() {
203+
_, sameNonce := input.MatchDurableNonce(otherNonce)
204+
if sameNonce {
205+
// one of the transactions will fail
206+
return false
207+
} else {
208+
// both work
209+
return true
172210
}
173211
}
212+
// solana transactions are always independent if no durable nonce
174213
return true
175214
}
176215

177216
func (input *TxInput) SafeFromDoubleSend(other xc.TxInput) (safe bool) {
178-
if otherNonce, ok := other.(GetDurableNonceInfo); ok {
179-
sameAccount := !input.DurableNonceAccount.IsZero() &&
180-
input.DurableNonceAccount.Equals(otherNonce.GetDurableNonceAccount())
217+
otherInput, ok := other.(GetTxInfo)
218+
if !ok {
219+
return false
220+
}
221+
222+
if input.HasDurableNonce() {
223+
sameAccount, sameNonce := input.MatchDurableNonce(otherInput)
181224
if sameAccount {
182-
// Safe only when both have actual nonce values and they match
183-
// (the nonce can only be consumed once, so only one tx can land).
184-
// If either is missing a nonce (e.g. setup phase), not safe.
185-
if input.HasDurableNonce() && otherNonce.HasDurableNonce() {
186-
return input.DurableNonce.Equals(otherNonce.GetDurableNonceValue())
225+
if sameNonce {
226+
// safe
227+
return true
228+
} else {
229+
return false
187230
}
188-
return false
189231
}
190232
}
191233

192234
// For recent blockhash (non-durable-nonce) transactions
193-
oldInput, ok := other.(GetTxInfo)
194-
if !ok {
195-
return false
235+
if input.DidTimeoutOccur(otherInput) {
236+
return true
196237
}
197-
diff := input.Timestamp - oldInput.GetTimestamp()
198-
if diff < int64(SafetyTimeoutMargin.Seconds()) || oldInput.GetRecentBlockhash().Equals(input.GetRecentBlockhash()) {
199-
return false
200-
}
201-
return true
238+
239+
return false
202240
}
203241

204242
func (input *TxInput) SetUnix(unix int64) {

chain/solana/tx_input/tx_input_test.go

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -183,22 +183,24 @@ func TestTxInputConflicts(t *testing.T) {
183183
},
184184
}
185185
for i, v := range vectors {
186-
newBz, _ := json.Marshal(v.newInput)
187-
oldBz, _ := json.Marshal(v.oldInput)
188-
fmt.Printf("testcase %d - expect safe=%t, independent=%t\n newInput = %s\n oldInput = %s\n", i, v.doubleSpendSafe, v.independent, string(newBz), string(oldBz))
189-
fmt.Println()
190-
require.Equal(
191-
t,
192-
v.newInput.IndependentOf(v.oldInput),
193-
v.independent,
194-
"IndependentOf",
195-
)
196-
require.Equal(
197-
t,
198-
v.newInput.SafeFromDoubleSend(v.oldInput),
199-
v.doubleSpendSafe,
200-
"SafeFromDoubleSend",
201-
)
186+
t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) {
187+
newBz, _ := json.Marshal(v.newInput)
188+
oldBz, _ := json.Marshal(v.oldInput)
189+
fmt.Printf("testcase %d - expect safe=%t, independent=%t\n newInput = %s\n oldInput = %s\n", i, v.doubleSpendSafe, v.independent, string(newBz), string(oldBz))
190+
fmt.Println()
191+
require.Equal(
192+
t,
193+
v.newInput.IndependentOf(v.oldInput),
194+
v.independent,
195+
"IndependentOf",
196+
)
197+
require.Equal(
198+
t,
199+
v.newInput.SafeFromDoubleSend(v.oldInput),
200+
v.doubleSpendSafe,
201+
"SafeFromDoubleSend",
202+
)
203+
})
202204
}
203205
}
204206

tx.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ type TxInputWithUnix interface {
2020
SetUnix(int64)
2121
}
2222

23+
// For transactions that come with their own payload, like Calls.
24+
// This may change how conflict resolution works, like on Solana.
25+
type TxInputWithCall interface {
26+
SetCall(call TxCallPayload) error
27+
}
28+
2329
type TxInputGasFeeMultiplier interface {
2430
SetGasFeePriority(priority GasFeePriority) error
2531
}
@@ -215,4 +221,11 @@ type TxCall interface {
215221
// Indicate if it's possible & safe to retry the call transaction.
216222
// Meaning, can we re-sign the transaction with the a new input?
217223
IsRetryable() (ok bool, reason string)
224+
225+
// Used if the tx-input needs to be updated retro-actively, like on Solana.
226+
GetPayload() (TxCallPayload, bool)
227+
}
228+
229+
type TxCallPayload interface {
230+
IsTxCallPayload()
218231
}

0 commit comments

Comments
 (0)