From c94c2b304824be85ac23dbdb7d5791a17795da45 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 2 Feb 2024 16:49:13 -0500 Subject: [PATCH 1/2] Support versioned transactions --- pkg/jupiter/client.go | 22 +++-- pkg/solana/address_lookup_table.go | 25 ++++++ pkg/solana/encoding.go | 64 ++++++++++++++ pkg/solana/transaction.go | 132 +++++++++++++++++++++++++++-- 4 files changed, 226 insertions(+), 17 deletions(-) create mode 100644 pkg/solana/address_lookup_table.go diff --git a/pkg/jupiter/client.go b/pkg/jupiter/client.go index a4552e25..d5ee698a 100644 --- a/pkg/jupiter/client.go +++ b/pkg/jupiter/client.go @@ -51,6 +51,10 @@ func (q *Quote) GetEstimatedSwapAmount() uint64 { return q.estimatedSwapAmount } +func (q *Quote) String() string { + return q.jsonString +} + // GetQuote gets an optimal route for performing a swap func (c *Client) GetQuote( ctx context.Context, @@ -117,6 +121,7 @@ type SwapInstructions struct { SetupInstructions []solana.Instruction SwapInstruction solana.Instruction CleanupInstruction *solana.Instruction + AddressLookupTables []string } // GetSwapInstructions gets the instructions to construct a transaction to sign @@ -130,10 +135,6 @@ func (c *Client) GetSwapInstructions( tracer := metrics.TraceMethodCall(ctx, metricsStructName, "GetSwapInstructions") defer tracer.End() - if !quote.useLegacyInstructions { - return nil, errors.New("only legacy transactions are supported") - } - // todo: struct this reqBody := fmt.Sprintf( `{"quoteResponse": %s, "userPublicKey": "%s", "destinationTokenAccount": "%s", "prioritizationFeeLamports": "auto", "asLegacyTransaction": %v}`, @@ -206,6 +207,8 @@ func (c *Client) GetSwapInstructions( } } + res.AddressLookupTables = jsonBody.AddressLookupTableAddresses + return &res, nil } @@ -258,9 +261,10 @@ type jsonInstruction struct { } type jsonSwapInstructions struct { - TokenLedgerInstruction *jsonInstruction `json:"tokenLedgerInstruction"` - ComputeBudgetInstructions []*jsonInstruction `json:"computeBudgetInstructions"` - SetupInstructions []*jsonInstruction `json:"setupInstructions"` - SwapInstruction *jsonInstruction `json:"swapInstruction"` - CleanupInstruction *jsonInstruction `json:"cleanupInstruction"` + TokenLedgerInstruction *jsonInstruction `json:"tokenLedgerInstruction"` + ComputeBudgetInstructions []*jsonInstruction `json:"computeBudgetInstructions"` + SetupInstructions []*jsonInstruction `json:"setupInstructions"` + SwapInstruction *jsonInstruction `json:"swapInstruction"` + CleanupInstruction *jsonInstruction `json:"cleanupInstruction"` + AddressLookupTableAddresses []string `json:"addressLookupTableAddresses"` } diff --git a/pkg/solana/address_lookup_table.go b/pkg/solana/address_lookup_table.go new file mode 100644 index 00000000..7a0525f7 --- /dev/null +++ b/pkg/solana/address_lookup_table.go @@ -0,0 +1,25 @@ +package solana + +import ( + "bytes" + "crypto/ed25519" +) + +type AddressLookupTable struct { + PublicKey ed25519.PublicKey + Addresses []ed25519.PublicKey +} + +type SortableAddressLookupTables []AddressLookupTable + +func (s SortableAddressLookupTables) Len() int { + return len(s) +} + +func (s SortableAddressLookupTables) Less(i int, j int) bool { + return bytes.Compare(s[i].PublicKey, s[j].PublicKey) < 0 +} + +func (s SortableAddressLookupTables) Swap(i int, j int) { + s[i], s[j] = s[j], s[i] +} diff --git a/pkg/solana/encoding.go b/pkg/solana/encoding.go index 2bac132a..9f717664 100644 --- a/pkg/solana/encoding.go +++ b/pkg/solana/encoding.go @@ -49,6 +49,17 @@ func (t *Transaction) Unmarshal(b []byte) error { } func (m Message) Marshal() []byte { + switch m.version { + case MessageVersionLegacy: + return m.marshalLegacy() + case MessageVersion0: + return m.marshalV0() + default: + panic("unsupported message version") + } +} + +func (m Message) marshalLegacy() []byte { b := bytes.NewBuffer(nil) // Header @@ -82,7 +93,60 @@ func (m Message) Marshal() []byte { return b.Bytes() } +func (m Message) marshalV0() []byte { + b := bytes.NewBuffer(nil) + + // Version Number + _ = b.WriteByte(byte(m.version + 127)) + + // Header + _ = b.WriteByte(m.Header.NumSignatures) + _ = b.WriteByte(m.Header.NumReadonlySigned) + _ = b.WriteByte(m.Header.NumReadOnly) + + // Accounts + _, _ = shortvec.EncodeLen(b, len(m.Accounts)) + for _, a := range m.Accounts { + _, _ = b.Write(a) + } + + // Recent Blockhash + _, _ = b.Write(m.RecentBlockhash[:]) + + // Instructions + _, _ = shortvec.EncodeLen(b, len(m.Instructions)) + for _, i := range m.Instructions { + _ = b.WriteByte(i.ProgramIndex) + + // Accounts + _, _ = shortvec.EncodeLen(b, len(i.Accounts)) + _, _ = b.Write(i.Accounts) + + // Data + _, _ = shortvec.EncodeLen(b, len(i.Data)) + _, _ = b.Write(i.Data) + } + + _, _ = shortvec.EncodeLen(b, len(m.AddressTableLookups)) + for _, addressTableLookup := range m.AddressTableLookups { + _, _ = b.Write(addressTableLookup.PublicKey) + + _, _ = shortvec.EncodeLen(b, len(addressTableLookup.WritableIndexes)) + _, _ = b.Write(addressTableLookup.WritableIndexes) + + _, _ = shortvec.EncodeLen(b, len(addressTableLookup.ReadonlyIndexes)) + _, _ = b.Write(addressTableLookup.ReadonlyIndexes) + } + + return b.Bytes() +} + func (m *Message) Unmarshal(b []byte) (err error) { + // todo: double check this is correct + if b[0] > 127 { + return errors.New("versioned messages not supported") + } + buf := bytes.NewBuffer(b) // Header diff --git a/pkg/solana/transaction.go b/pkg/solana/transaction.go index e5768801..2703053b 100644 --- a/pkg/solana/transaction.go +++ b/pkg/solana/transaction.go @@ -20,17 +20,32 @@ const ( type Signature [ed25519.SignatureSize]byte type Blockhash [sha256.Size]byte +type MessageVersion uint8 + +const ( + MessageVersionLegacy MessageVersion = iota + MessageVersion0 +) + type Header struct { NumSignatures byte NumReadonlySigned byte NumReadOnly byte } +type MessageAddressTableLookup struct { + PublicKey ed25519.PublicKey + WritableIndexes []byte + ReadonlyIndexes []byte +} + type Message struct { - Header Header - Accounts []ed25519.PublicKey - RecentBlockhash Blockhash - Instructions []CompiledInstruction + version MessageVersion + Header Header + Accounts []ed25519.PublicKey + RecentBlockhash Blockhash + Instructions []CompiledInstruction + AddressTableLookups []MessageAddressTableLookup } type Transaction struct { @@ -39,6 +54,15 @@ type Transaction struct { } func NewTransaction(payer ed25519.PublicKey, instructions ...Instruction) Transaction { + return newTransaction(payer, nil, instructions) +} + +func NewVersionedTransaction(payer ed25519.PublicKey, addressLookupTables []AddressLookupTable, instructions []Instruction) Transaction { + return newTransaction(payer, addressLookupTables, instructions) +} + +// todo: consolidate to new constructor +func newTransaction(payer ed25519.PublicKey, addressLookupTables []AddressLookupTable, instructions []Instruction) Transaction { accounts := []AccountMeta{ { PublicKey: payer, @@ -65,8 +89,45 @@ func NewTransaction(payer ed25519.PublicKey, instructions ...Instruction) Transa accounts = filterUnique(accounts) sort.Sort(SortableAccountMeta(accounts)) + // Sort address tables to guarantee consistent marshalling + sortedAddressLookupTables := make([]AddressLookupTable, len(addressLookupTables)) + copy(sortedAddressLookupTables, addressLookupTables) + sort.Sort(SortableAddressLookupTables(sortedAddressLookupTables)) + + writableAddressTableIndexes := make([][]byte, len(sortedAddressLookupTables)) + readonlyAddressTableIndexes := make([][]byte, len(sortedAddressLookupTables)) + var m Message for _, account := range accounts { + // If the account is eligible for dynamic loading, then pull its index + // from the first address table where it's defined. + var isDynamicallyLoaded bool + if !account.isPayer && !account.IsSigner && !account.isProgram { + for i, addressLookupTable := range sortedAddressLookupTables { + for j, address := range addressLookupTable.Addresses { + if bytes.Equal(address, account.PublicKey) { + isDynamicallyLoaded = true + + if account.IsWritable { + writableAddressTableIndexes[i] = append(writableAddressTableIndexes[i], byte(j)) + } else { + readonlyAddressTableIndexes[i] = append(readonlyAddressTableIndexes[i], byte(j)) + } + + break + } + } + + if isDynamicallyLoaded { + break + } + } + } + if isDynamicallyLoaded { + continue + } + + // Otherwise, the account is defined statically m.Accounts = append(m.Accounts, account.PublicKey) if account.IsSigner { @@ -80,21 +141,58 @@ func NewTransaction(payer ed25519.PublicKey, instructions ...Instruction) Transa } } + // Consolidate static and dynamically loaded accounts into an ordered list, + // which is used for index references encoded in the message + dynamicWritableAccounts := make([]ed25519.PublicKey, 0) + dynamicReadonlyAccount := make([]ed25519.PublicKey, 0) + for i, writableAddressTableIndexes := range writableAddressTableIndexes { + for _, index := range writableAddressTableIndexes { + writableAccount := sortedAddressLookupTables[i].Addresses[index] + dynamicWritableAccounts = append(dynamicWritableAccounts, writableAccount) + } + } + for i, readonlyAddressTableIndexes := range readonlyAddressTableIndexes { + for _, index := range readonlyAddressTableIndexes { + readonlyAccount := sortedAddressLookupTables[i].Addresses[index] + dynamicReadonlyAccount = append(dynamicReadonlyAccount, readonlyAccount) + } + } + var allAccounts []ed25519.PublicKey + allAccounts = append(allAccounts, m.Accounts...) + allAccounts = append(allAccounts, dynamicWritableAccounts...) + allAccounts = append(allAccounts, dynamicReadonlyAccount...) + // Generate the compiled instruction, which uses indices instead // of raw account keys. for _, i := range instructions { c := CompiledInstruction{ - ProgramIndex: byte(indexOf(m.Accounts, i.Program)), + ProgramIndex: byte(indexOf(allAccounts, i.Program)), Data: i.Data, } for _, a := range i.Accounts { - c.Accounts = append(c.Accounts, byte(indexOf(m.Accounts, a.PublicKey))) + c.Accounts = append(c.Accounts, byte(indexOf(allAccounts, a.PublicKey))) } m.Instructions = append(m.Instructions, c) } + // Generate the compiled message address table lookups + for i, addressLookupTable := range sortedAddressLookupTables { + if len(writableAddressTableIndexes[i]) == 0 && len(readonlyAddressTableIndexes[i]) == 0 { + continue + } + + m.AddressTableLookups = append(m.AddressTableLookups, MessageAddressTableLookup{ + PublicKey: addressLookupTable.PublicKey, + WritableIndexes: writableAddressTableIndexes[i], + ReadonlyIndexes: readonlyAddressTableIndexes[i], + }) + } + if len(m.AddressTableLookups) > 0 { + m.version = MessageVersion0 + } + for i := range m.Accounts { if len(m.Accounts[i]) == 0 { m.Accounts[i] = make([]byte, ed25519.PublicKeySize) @@ -118,11 +216,12 @@ func (t *Transaction) String() string { sb.WriteString(fmt.Sprintf(" %d: %s\n", i, base58.Encode(s[:]))) } sb.WriteString("Message:\n") + sb.WriteString(fmt.Sprintf(" Version: %s\n", t.Message.version.String())) sb.WriteString(" Header:\n") sb.WriteString(fmt.Sprintf(" NumSignatures: %d\n", t.Message.Header.NumSignatures)) sb.WriteString(fmt.Sprintf(" NumReadOnly: %d\n", t.Message.Header.NumReadOnly)) sb.WriteString(fmt.Sprintf(" NumReadOnlySigned: %d\n", t.Message.Header.NumReadonlySigned)) - sb.WriteString(" Accounts:\n") + sb.WriteString(" Static Accounts:\n") for i, a := range t.Message.Accounts { sb.WriteString(fmt.Sprintf(" %d: %s\n", i, base58.Encode(a))) } @@ -133,7 +232,14 @@ func (t *Transaction) String() string { sb.WriteString(fmt.Sprintf(" Accounts: %v\n", t.Message.Instructions[i].Accounts)) sb.WriteString(fmt.Sprintf(" Data: %v\n", t.Message.Instructions[i].Data)) } - + if len(t.Message.AddressTableLookups) > 0 { + sb.WriteString(" Address Table Lookups:\n") + for i := range t.Message.AddressTableLookups { + sb.WriteString(fmt.Sprintf(" %s:\n", base58.Encode(t.Message.AddressTableLookups[i].PublicKey))) + sb.WriteString(fmt.Sprintf(" Writable Indexes: %v\n", t.Message.AddressTableLookups[i].WritableIndexes)) + sb.WriteString(fmt.Sprintf(" Readonly Indexes: %v\n", t.Message.AddressTableLookups[i].ReadonlyIndexes)) + } + } return sb.String() } @@ -198,3 +304,13 @@ func indexOf(slice []ed25519.PublicKey, item ed25519.PublicKey) int { return -1 } + +func (v MessageVersion) String() string { + switch v { + case MessageVersionLegacy: + return "legacy" + case MessageVersion0: + return "v0" + } + return "unknown" +} From c9bce10a12fd9cf88b0c4c681bb7065017da5f8f Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 5 Feb 2024 10:44:57 -0500 Subject: [PATCH 2/2] Add support for ALTs --- pkg/solana/addresslookuptable/accounts.go | 62 +++++++++++++++++++++++ pkg/solana/binary/utils.go | 29 ++++++----- pkg/solana/token/state.go | 14 ++--- 3 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 pkg/solana/addresslookuptable/accounts.go diff --git a/pkg/solana/addresslookuptable/accounts.go b/pkg/solana/addresslookuptable/accounts.go new file mode 100644 index 00000000..790a79b9 --- /dev/null +++ b/pkg/solana/addresslookuptable/accounts.go @@ -0,0 +1,62 @@ +package address_lookup_table + +import ( + "crypto/ed25519" + "errors" + "fmt" + + "github.com/mr-tron/base58" + + "github.com/code-payments/code-server/pkg/solana/binary" +) + +var ( + ErrInvalidAccountSize = errors.New("invalid address lookup table account size") +) + +const optionSize = 1 + +const metadataSize = 56 +const maxAddresses = 256 + +type AddressLookupTableAccount struct { + TypeIndex uint32 + DeactivationSlot uint64 + LastExtendedSlot uint64 + LastExtendedSlotStartIndex uint8 + Authority ed25519.PublicKey + Addresses []ed25519.PublicKey +} + +func (obj *AddressLookupTableAccount) Unmarshal(data []byte) error { + var offset int + + if len(data) < metadataSize { + return ErrInvalidAccountSize + } + + binary.GetUint32(data[offset:], &obj.TypeIndex, &offset) + binary.GetUint64(data[offset:], &obj.DeactivationSlot, &offset) + binary.GetUint64(data[offset:], &obj.LastExtendedSlot, &offset) + binary.GetUint8(data[offset:], &obj.LastExtendedSlotStartIndex, &offset) + binary.GetOptionalKey32(data[offset:], &obj.Authority, &offset, optionSize) + + fmt.Println(base58.Encode(obj.Authority)) + + offset = 56 + + addressBufferSize := len(data) - offset + addressCount := addressBufferSize / 32 + if addressBufferSize%ed25519.PublicKeySize != 0 { + return ErrInvalidAccountSize + } else if addressCount > maxAddresses { + return ErrInvalidAccountSize + } + + obj.Addresses = make([]ed25519.PublicKey, addressCount) + for i := 0; i < addressCount; i++ { + binary.GetKey32(data[offset:], &obj.Addresses[i], &offset) + } + + return nil +} diff --git a/pkg/solana/binary/utils.go b/pkg/solana/binary/utils.go index ddf464cd..73ba777d 100644 --- a/pkg/solana/binary/utils.go +++ b/pkg/solana/binary/utils.go @@ -10,13 +10,13 @@ func PutKey32(dst []byte, src []byte, offset *int) { *offset += ed25519.PublicKeySize } -func PutOptionalKey32(dst []byte, src []byte, offset *int) { +func PutOptionalKey32(dst []byte, src []byte, offset *int, optionSize int) { if len(src) > 0 { dst[0] = 1 - copy(dst[4:], src) + copy(dst[optionSize:], src) } - *offset += 4 + ed25519.PublicKeySize + *offset += optionSize + ed25519.PublicKeySize } func PutUint64(dst []byte, v uint64, offset *int) { @@ -29,12 +29,12 @@ func PutUint32(dst []byte, v uint32, offset *int) { *offset += 4 } -func PutOptionalUint64(dst []byte, v *uint64, offset *int) { +func PutOptionalUint64(dst []byte, v *uint64, offset *int, optionSize int) { if v != nil { dst[0] = 1 - binary.LittleEndian.PutUint64(dst[4:], *v) + binary.LittleEndian.PutUint64(dst[optionSize:], *v) } - *offset += 4 + 8 + *offset += optionSize + 8 } func GetKey32(src []byte, dst *ed25519.PublicKey, offset *int) { @@ -43,12 +43,12 @@ func GetKey32(src []byte, dst *ed25519.PublicKey, offset *int) { *offset += ed25519.PublicKeySize } -func GetOptionalKey32(src []byte, dst *ed25519.PublicKey, offset *int) { +func GetOptionalKey32(src []byte, dst *ed25519.PublicKey, offset *int, optionSize int) { if src[0] == 1 { *dst = make([]byte, ed25519.PublicKeySize) - copy(*dst, src[4:]) + copy(*dst, src[optionSize:]) } - *offset += 4 + ed25519.PublicKeySize + *offset += optionSize + ed25519.PublicKeySize } func GetUint64(src []byte, dst *uint64, offset *int) { @@ -61,10 +61,15 @@ func GetUint32(src []byte, dst *uint32, offset *int) { *offset += 4 } -func GetOptionalUint64(src []byte, dst **uint64, offset *int) { +func GetUint8(src []byte, dst *uint8, offset *int) { + *dst = src[0] + *offset += 1 +} + +func GetOptionalUint64(src []byte, dst **uint64, offset *int, optionSize int) { if src[0] == 1 { - val := binary.LittleEndian.Uint64(src[4:]) + val := binary.LittleEndian.Uint64(src[optionSize:]) *dst = &val } - *offset += 4 + 8 + *offset += optionSize + 8 } diff --git a/pkg/solana/token/state.go b/pkg/solana/token/state.go index 62d5c258..86b243e5 100644 --- a/pkg/solana/token/state.go +++ b/pkg/solana/token/state.go @@ -20,6 +20,8 @@ const AccountSize = 165 // Reference: https://github.com/solana-labs/solana-program-library/blob/8944f428fe693c3a4226bf766a79be9c75e8e520/token/program/src/state.rs#L214 const MultisigAccountSize = 355 +const optionSize = 4 + type Account struct { // The mint associated with this account Mint ed25519.PublicKey @@ -49,12 +51,12 @@ func (a *Account) Marshal() []byte { binary.PutKey32(b, a.Mint, &offset) binary.PutKey32(b[offset:], a.Owner, &offset) binary.PutUint64(b[offset:], a.Amount, &offset) - binary.PutOptionalKey32(b[offset:], a.Delegate, &offset) + binary.PutOptionalKey32(b[offset:], a.Delegate, &offset, optionSize) b[offset] = byte(a.State) offset++ - binary.PutOptionalUint64(b[offset:], a.IsNative, &offset) + binary.PutOptionalUint64(b[offset:], a.IsNative, &offset, optionSize) binary.PutUint64(b[offset:], a.DelegatedAmount, &offset) - binary.PutOptionalKey32(b[offset:], a.CloseAuthority, &offset) + binary.PutOptionalKey32(b[offset:], a.CloseAuthority, &offset, optionSize) return b } @@ -68,12 +70,12 @@ func (a *Account) Unmarshal(b []byte) bool { binary.GetKey32(b, &a.Mint, &offset) binary.GetKey32(b[offset:], &a.Owner, &offset) binary.GetUint64(b[offset:], &a.Amount, &offset) - binary.GetOptionalKey32(b[offset:], &a.Delegate, &offset) + binary.GetOptionalKey32(b[offset:], &a.Delegate, &offset, optionSize) a.State = AccountState(b[offset]) offset++ - binary.GetOptionalUint64(b[offset:], &a.IsNative, &offset) + binary.GetOptionalUint64(b[offset:], &a.IsNative, &offset, optionSize) binary.GetUint64(b[offset:], &a.DelegatedAmount, &offset) - binary.GetOptionalKey32(b[offset:], &a.CloseAuthority, &offset) + binary.GetOptionalKey32(b[offset:], &a.CloseAuthority, &offset, optionSize) return true }