Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions apps/solana/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package solana
import (
"context"
"crypto/ed25519"
"encoding/base64"
"errors"
"fmt"
"math/big"
"time"
Expand All @@ -20,11 +22,33 @@ import (
"github.com/gagliardetto/solana-go/programs/token"
)

var ErrTransactionTooLarge = errors.New("solana transaction too large")

type transactionTooLargeError struct {
encodedSize int
}

func (e *transactionTooLargeError) Error() string {
return fmt.Sprintf(
"base64 encoded solana_transaction::versioned::VersionedTransaction too large: %d bytes (max: encoded/raw %d/%d)",
e.encodedSize,
MaxTransactionEncodedSize,
MaxTransactionRawSize,
)
}

func (e *transactionTooLargeError) Unwrap() error {
return ErrTransactionTooLarge
}

const (
NonceAccountSize uint64 = 80
MintSize uint64 = 82
NormalAccountSize uint64 = 165

MaxTransactionRawSize = 1232
MaxTransactionEncodedSize = 1644

maxNameLength = 32
maxSymbolLength = 10

Expand All @@ -36,6 +60,28 @@ const (
AssetDecimal = 8
)

// ValidateTransactionSize checks the wire size of a fully signed transaction.
// Transactions created for system calls are commonly marshaled before their
// signatures are produced, so counting tx.Signatures directly can undercount
// the final wire size by 64 bytes per missing signature.
func ValidateTransactionSize(tx *solana.Transaction) error {
if tx == nil {
return fmt.Errorf("nil solana transaction")
}

fullySigned := *tx
fullySigned.Signatures = make([]solana.Signature, int(tx.Message.Header.NumRequiredSignatures))
raw, err := fullySigned.MarshalBinary()
if err != nil {
return fmt.Errorf("marshal solana transaction: %w", err)
}
if len(raw) <= MaxTransactionRawSize {
return nil
}

return &transactionTooLargeError{encodedSize: base64.StdEncoding.EncodedLen(len(raw))}
}

type Metadata struct {
Name string `json:"name"`
Symbol string `json:"symbol"`
Expand Down
3 changes: 3 additions & 0 deletions apps/solana/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ func (c *Client) GetMint(ctx context.Context, mint solana.PublicKey) (*token.Min
}

func (c *Client) SendTransaction(ctx context.Context, tx *solana.Transaction) (string, error) {
if err := ValidateTransactionSize(tx); err != nil {
return "", err
}
sig, err := c.rpcClient.SendTransactionWithOpts(ctx, tx, rpc.TransactionOpts{
SkipPreflight: true,
PreflightCommitment: rpc.CommitmentProcessed,
Expand Down
24 changes: 23 additions & 1 deletion apps/solana/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ func (c *Client) CreateNonceAccount(ctx context.Context, key, nonce string, rent
if err != nil {
panic(err)
}
if err := ValidateTransactionSize(tx); err != nil {
return nil, err
}
return tx, nil
}

Expand Down Expand Up @@ -102,6 +105,9 @@ func (c *Client) InitializeAccount(ctx context.Context, key, user string) (*sola
if err != nil {
panic(err)
}
if err := ValidateTransactionSize(tx); err != nil {
return nil, err
}
return tx, nil
}

Expand Down Expand Up @@ -189,6 +195,9 @@ func (c *Client) CreateMints(ctx context.Context, payer, mtg solana.PublicKey, a
panic(err)
}
}
if err := ValidateTransactionSize(tx); err != nil {
return nil, err
}
return tx, nil
}

Expand Down Expand Up @@ -232,6 +241,9 @@ func (c *Client) ExtendLookupTables(ctx context.Context, key, table string, as [
if err != nil {
panic(err)
}
if err := ValidateTransactionSize(tx); err != nil {
return nil, "", err
}
return tx, table, nil
}

Expand Down Expand Up @@ -283,6 +295,9 @@ func (c *Client) TransferOrMintTokens(ctx context.Context, payer, mtg solana.Pub
if err != nil {
panic(err)
}
if err := ValidateTransactionSize(tx); err != nil {
return nil, err
}
return tx, nil
}

Expand Down Expand Up @@ -312,7 +327,14 @@ func (c *Client) TransferOrBurnTokens(ctx context.Context, payer, user solana.Pu
)
}

return builder.Build()
tx, err := builder.Build()
if err != nil {
return nil, err
}
if err := ValidateTransactionSize(tx); err != nil {
return nil, err
}
return tx, nil
}

func (c *Client) AddTransferSolanaAssetInstruction(ctx context.Context, builder *solana.TransactionBuilder, transfer *TokenTransfer, payer, source solana.PublicKey) (*solana.TransactionBuilder, error) {
Expand Down
53 changes: 35 additions & 18 deletions solana/observer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"math"
"strings"
Expand Down Expand Up @@ -620,43 +621,47 @@ func (node *Node) handleUnconfirmedCalls(ctx context.Context) error {
}
}

if failureReason != "" {
logger.Printf("observer.expireSystemCall(%v %v %s)", call, nonce, failureReason)
id = common.UniqueId(id, "expire-nonce")
extra[0] = ConfirmFlagNonceExpired
err = node.store.WriteFailedCallIfNotExist(ctx, call, failureReason)
if err != nil {
return err
}
} else {
err := node.OccupyNonceAccountByCall(ctx, nonce, call.RequestId)
if failureReason == "" {
err = node.OccupyNonceAccountByCall(ctx, nonce, call.RequestId)
if err != nil {
return err
}

cid := common.UniqueId(id, "storage")
fee, err := node.getSystemCallFeeFromXIN(ctx, call)
if err != nil {
return err
}
nonce = node.ReadSpareNonceAccountWithCall(ctx, cid)
tx, err := node.CreatePrepareTransaction(ctx, call, nonce, fee)
if err != nil {
cid := common.UniqueId(id, "storage")
prepareNonce := node.ReadSpareNonceAccountWithCall(ctx, cid)
prepareTx, err := node.CreatePrepareTransaction(ctx, call, prepareNonce, fee)
if errors.Is(err, solanaApp.ErrTransactionTooLarge) {
failureReason = err.Error()
} else if err != nil {
return err
}
if tx != nil {
err := node.OccupyNonceAccountByCall(ctx, nonce, cid)

if prepareTx != nil {
err := node.OccupyNonceAccountByCall(ctx, prepareNonce, cid)
if err != nil {
return err
}
tb, err := tx.MarshalBinary()
tb, err := prepareTx.MarshalBinary()
if err != nil {
panic(err)
}
extra = attachSystemCall(extra, cid, tb)
}
}

if failureReason != "" {
logger.Printf("observer.expireSystemCall(%v %v %s)", call, nonce, failureReason)
id = common.UniqueId(id, "expire-nonce")
extra[0] = ConfirmFlagNonceExpired
err = node.store.WriteFailedCallIfNotExist(ctx, call, failureReason)
if err != nil {
return err
}
}

err = node.sendObserverTransactionToGroup(ctx, &common.Operation{
Id: id,
Type: OperationTypeConfirmNonce,
Expand Down Expand Up @@ -728,6 +733,18 @@ func (node *Node) handleSignedCallSequence(ctx context.Context, wg *sync.WaitGro
var ids []string
for _, c := range calls {
ids = append(ids, c.RequestId)
tx, err := solana.TransactionFromBase64(c.Raw)
if err != nil {
panic(fmt.Errorf("solana.TransactionFromBase64(%s) => %v", c.RequestId, err))
}
err = solanaApp.ValidateTransactionSize(tx)
if errors.Is(err, solanaApp.ErrTransactionTooLarge) {
logger.Printf("node.handleSignedCallSequence(%s) => skip oversized call %s: %v", key, c.RequestId, err)
return
}
if err != nil {
panic(fmt.Errorf("solana.ValidateTransactionSize(%s) => %v", c.RequestId, err))
}
}
logger.Printf("node.handleSignedCallSequence(%s) => %s", key, strings.Join(ids, ","))
if len(calls) > 2 {
Expand Down
6 changes: 3 additions & 3 deletions solana/solana.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,13 +314,13 @@ func (node *Node) solanaProcessDepositTransaction(ctx context.Context, depositHa
}

nonce := node.ReadSpareNonceAccountWithCall(ctx, cid)
err = node.store.OccupyNonceAccountByCall(ctx, nonce.Address, cid)
tx, err := node.solana.TransferOrBurnTokens(ctx, node.SolanaPayer(), solana.MustPublicKeyFromBase58(user), nonce.Account(), ts)
if err != nil {
return err
}
tx, err := node.solana.TransferOrBurnTokens(ctx, node.SolanaPayer(), solana.MustPublicKeyFromBase58(user), nonce.Account(), ts)
err = node.store.OccupyNonceAccountByCall(ctx, nonce.Address, cid)
if err != nil {
panic(err)
return err
}
data, err := tx.MarshalBinary()
if err != nil {
Expand Down
10 changes: 8 additions & 2 deletions solana/system_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,14 +463,20 @@ func (node *Node) getSubSystemCallFromExtra(ctx context.Context, req *store.Requ
return node.buildSystemCallFromBytes(ctx, req, id, raw, true)
}

// should only return error when fail to resolve address lookups or parse nonce advance instruction;
// without fields of superior, type, public, skip_postprocess
// Returns validation errors for oversized transactions, unresolved address
// lookups, or malformed nonce-advance instructions. The returned call omits
// superior, type, public, and skip_postprocess fields.
func (node *Node) buildSystemCallFromBytes(ctx context.Context, req *store.Request, id string, raw []byte, withdrawn bool) (*store.SystemCall, *solana.Transaction, error) {
tx, err := solana.TransactionFromBytes(raw)
logger.Printf("solana.TransactionFromBytes(%x) => %v %v", raw, tx, err)
if err != nil {
return nil, nil, err
}
err = solanaApp.ValidateTransactionSize(tx)
if err != nil {
logger.Printf("solana.ValidateTransactionSize(%s %s) => %v", req.Id, id, err)
return nil, nil, err
}
err = node.processTransactionWithAddressLookups(ctx, tx)
if err != nil {
if errors.Is(err, errInvalidAddressLookup) {
Expand Down
56 changes: 45 additions & 11 deletions store/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ package store
import (
"context"
"database/sql"
"errors"
"fmt"
"time"

solanaApp "github.com/MixinNetwork/computer/apps/solana"
"github.com/MixinNetwork/mixin/logger"
"github.com/MixinNetwork/safe/common"
"github.com/gagliardetto/solana-go"
)

const (
oversizedDepositMigrationKey = "SCHEMA:VERSION:OVERSIZED_DEPOSIT_7E823E4C"
oversizedDepositSystemCallID = "7e823e4c-b389-320a-b241-ff96c30d730b"
)

func (s *SQLite3Store) Migrate(ctx context.Context) error {
Expand All @@ -19,24 +28,49 @@ func (s *SQLite3Store) Migrate(ctx context.Context) error {
}
defer common.Rollback(tx)

key, val := "SCHEMA:VERSION:FAILED_BURN", ""
row := tx.QueryRowContext(ctx, "SELECT value FROM properties WHERE key=?", key)
err = row.Scan(&val)
if err == nil || err != sql.ErrNoRows {
err = s.migrateOversizedDepositSystemCall(ctx, tx)
if err != nil {
return err
}

return tx.Commit()
}

func (s *SQLite3Store) migrateOversizedDepositSystemCall(ctx context.Context, tx *sql.Tx) error {
applied, err := s.checkExistence(ctx, tx, "SELECT value FROM properties WHERE key=?", oversizedDepositMigrationKey)
if err != nil || applied {
return err
}
now := time.Now().UTC()

query := "UPDATE system_calls SET state=? WHERE id=? AND state=?"
_, err = tx.ExecContext(ctx, query, common.RequestStatePending, "035d4b18-451d-336c-abf1-ee9909f4e931", common.RequestStateFailed)
call, err := s.ReadSystemCallByRequestId(ctx, oversizedDepositSystemCallID, common.RequestStatePending)
if err != nil {
return fmt.Errorf("SQLite3Store UPDATE system_calls %v", err)
return fmt.Errorf("store.ReadSystemCallByRequestId(%s) => %v", oversizedDepositSystemCallID, err)
}
if call == nil {
return s.writeProperty(ctx, tx, oversizedDepositMigrationKey, "system call not found")
}
if call.Type != CallTypeDeposit {
return fmt.Errorf("invalid system call type for oversized deposit migration: %s", call.Type)
}

_, err = tx.ExecContext(ctx, "INSERT INTO properties (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)", key, query, now, now)
solanaTx, err := solana.TransactionFromBase64(call.Raw)
if err != nil {
return err
return fmt.Errorf("solana.TransactionFromBase64(%s) => %v", oversizedDepositSystemCallID, err)
}
sizeErr := solanaApp.ValidateTransactionSize(solanaTx)
if sizeErr == nil {
return s.writeProperty(ctx, tx, oversizedDepositMigrationKey, "transaction within size limit")
}
if !errors.Is(sizeErr, solanaApp.ErrTransactionTooLarge) {
return fmt.Errorf("solana.ValidateTransactionSize(%s) => %v", oversizedDepositSystemCallID, sizeErr)
}
logger.Printf("store.migrateOversizedDepositSystemCall(%s) => %v", oversizedDepositSystemCallID, sizeErr)

return tx.Commit()
query := "UPDATE system_calls SET state=?, updated_at=? WHERE id=? AND call_type=? AND state=?"
err = s.execOne(ctx, tx, query, common.RequestStateFailed, time.Now().UTC(), oversizedDepositSystemCallID, CallTypeDeposit, common.RequestStatePending)
if err != nil {
return fmt.Errorf("SQLite3Store UPDATE oversized deposit system_calls %v", err)
}

return s.writeProperty(ctx, tx, oversizedDepositMigrationKey, sizeErr.Error())
}
Loading