Skip to content
Closed
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
3 changes: 3 additions & 0 deletions apps/solana/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@
CreatedAt time.Time

Uri string
Asset *bot.AssetNetwork

Check failure on line 54 in apps/solana/common.go

View workflow job for this annotation

GitHub Actions / lint

undefined: bot (typecheck)
PrivateKey *solana.PrivateKey

Check failure on line 55 in apps/solana/common.go

View workflow job for this annotation

GitHub Actions / lint

undefined: solana (typecheck)
}

type NonceAccount struct {
Address solana.PublicKey

Check failure on line 59 in apps/solana/common.go

View workflow job for this annotation

GitHub Actions / lint

undefined: solana (typecheck)
Hash solana.Hash

Check failure on line 60 in apps/solana/common.go

View workflow job for this annotation

GitHub Actions / lint

undefined: solana (typecheck)
}

type TokenTransfer struct {
Expand Down Expand Up @@ -414,6 +414,9 @@
}

func NonceAccountFromTx(tx *solana.Transaction) (*system.AdvanceNonceAccount, error) {
if len(tx.Message.Instructions) == 0 {
return nil, fmt.Errorf("transaction has no instructions")
}
ins := tx.Message.Instructions[0]
accounts, err := ins.ResolveInstructionAccounts(&tx.Message)
if err != nil {
Expand Down
66 changes: 58 additions & 8 deletions apps/solana/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"github.com/shopspring/decimal"
)

const solanaInnerIndexBase = int64(1_000_000_000)

func (c *Client) CreateNonceAccount(ctx context.Context, key, nonce string, rent uint64) (*solana.Transaction, error) {
payer, err := solana.PrivateKeyFromBase58(key)
if err != nil {
Expand Down Expand Up @@ -388,12 +390,19 @@ func (c *Client) getPriorityFeeInstruction(ctx context.Context) *computebudget.I
if err != nil {
panic(err)
}
fee := getAveragePriorityFee(recentFees)
return computebudget.NewSetComputeUnitPriceInstruction(fee).Build()
}

func getAveragePriorityFee(recentFees []rpc.PriorizationFeeResult) uint64 {
if len(recentFees) == 0 {
return 1000
}
total := decimal.NewFromInt(0)
for _, fee := range recentFees {
total = total.Add(decimal.NewFromUint64(fee.PrioritizationFee))
}
fee := total.Div(decimal.NewFromInt(int64(len(recentFees)))).BigInt().Uint64()
return computebudget.NewSetComputeUnitPriceInstruction(fee).Build()
return total.Div(decimal.NewFromInt(int64(len(recentFees)))).BigInt().Uint64()
}

func ExtractTransfersFromTransaction(ctx context.Context, tx *solana.Transaction, meta *rpc.TransactionMeta, exception *solana.PublicKey) ([]*Transfer, error) {
Expand Down Expand Up @@ -440,13 +449,12 @@ func ExtractTransfersFromTransaction(ctx context.Context, tx *solana.Transaction
}

for index, ix := range msg.Instructions {
baseIndex := int64(index+1) * 10000
if transfer := extractTransfersFromInstruction(&msg, ix, tokenAccounts, owners, transfers); transfer != nil {
if exception != nil && exception.String() == transfer.Receiver {
continue
}
transfer.Signature = hash
transfer.Index = baseIndex
transfer.Index = int64(index)
transfers = append(transfers, transfer)
}

Expand All @@ -456,7 +464,7 @@ func ExtractTransfersFromTransaction(ctx context.Context, tx *solana.Transaction
continue
}
transfer.Signature = hash
transfer.Index = baseIndex + int64(innerIndex) + 1
transfer.Index = (int64(index)+1)*solanaInnerIndexBase + int64(innerIndex)
transfers = append(transfers, transfer)
}
}
Expand All @@ -472,10 +480,23 @@ func ExtractTransferFromTransactionByIndex(ctx context.Context, tx *solana.Trans
msg := tx.Message

var (
tokenAccounts = map[solana.PublicKey]token.Account{}
owners = []*solana.PublicKey{}
innerInstructions = map[uint16][]solana.CompiledInstruction{}
tokenAccounts = map[solana.PublicKey]token.Account{}
owners = []*solana.PublicKey{}
)

for _, inner := range meta.InnerInstructions {
sis := make([]solana.CompiledInstruction, len(inner.Instructions))
for idx, ii := range inner.Instructions {
sis[idx] = solana.CompiledInstruction{
ProgramIDIndex: ii.ProgramIDIndex,
Accounts: ii.Accounts,
Data: ii.Data,
}
}
innerInstructions[inner.Index] = sis
}

bs := meta.PreTokenBalances
bs = append(bs, meta.PostTokenBalances...)
for _, balance := range bs {
Expand All @@ -492,7 +513,36 @@ func ExtractTransferFromTransactionByIndex(ctx context.Context, tx *solana.Trans
}
}

return extractTransfersFromInstruction(&msg, msg.Instructions[index], tokenAccounts, owners, nil)
ix, ok := instructionByTransferIndex(&msg, innerInstructions, index)
if !ok {
return nil
}
return extractTransfersFromInstruction(&msg, ix, tokenAccounts, owners, nil)
}

func instructionByTransferIndex(msg *solana.Message, innerInstructions map[uint16][]solana.CompiledInstruction, index int64) (solana.CompiledInstruction, bool) {
if index < 0 {
return solana.CompiledInstruction{}, false
}

if index < solanaInnerIndexBase {
if index >= int64(len(msg.Instructions)) {
return solana.CompiledInstruction{}, false
}
return msg.Instructions[index], true
}

outerIndex := index/solanaInnerIndexBase - 1
if outerIndex < 0 || outerIndex >= int64(len(msg.Instructions)) {
return solana.CompiledInstruction{}, false
}

innerIndex := index % solanaInnerIndexBase
inners := innerInstructions[uint16(outerIndex)]
if innerIndex < 0 || innerIndex >= int64(len(inners)) {
return solana.CompiledInstruction{}, false
}
return inners[innerIndex], true
}

func ExtractMintsFromTransaction(tx *solana.Transaction) []string {
Expand Down
5 changes: 5 additions & 0 deletions solana/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package solana
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
Expand Down Expand Up @@ -270,6 +271,10 @@ func (node *Node) httpLockNonce(w http.ResponseWriter, r *http.Request, params m

err = node.store.LockNonceAccountWithMix(ctx, nonce.Address, body.Mix)
if err != nil {
if errors.Is(err, store.ErrNonceAccountLimit) {
common.RenderJSON(w, r, http.StatusTooManyRequests, map[string]any{"error": "nonce limit"})
return
}
common.RenderError(w, r, err)
return
}
Expand Down
94 changes: 79 additions & 15 deletions solana/mvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"encoding/hex"
"fmt"
"math/big"
"slices"
"strings"

"github.com/MixinNetwork/bot-api-go-client/v3"
Expand Down Expand Up @@ -53,16 +52,19 @@ func (node *Node) processAddUser(ctx context.Context, req *store.Request) ([]*mt
}

mix := string(req.ExtraBytes())
_, err = bot.NewMixAddressFromString(mix)
logger.Printf("common.NewAddressFromString(%s) => %v", mix, err)
mmix, err := bot.NewMixAddressFromString(mix)
logger.Printf("bot.NewMixAddressFromString(%s) => %v", mix, err)
if err != nil {
return node.failRequest(ctx, req, "")
}
if !checkUser(ctx, req, mmix) {
return node.failRequest(ctx, req, "")
}

old, err := node.store.ReadUserByMixAddress(ctx, mix)
logger.Printf("store.ReadUserByAddress(%s) => %v %v", mix, old, err)
logger.Printf("store.ReadUserByMixAddress(%s) => %v %v", mix, old, err)
if err != nil {
panic(fmt.Errorf("store.ReadUserByAddress(%s) => %v", mix, err))
panic(fmt.Errorf("store.ReadUserByMixAddress(%s) => %v", mix, err))
} else if old != nil {
return node.failRequest(ctx, req, "")
}
Expand Down Expand Up @@ -108,6 +110,13 @@ func (node *Node) processUserDeposit(ctx context.Context, req *store.Request) ([
} else if user == nil {
return node.failRequest(ctx, req, "")
}
mix, err := bot.NewMixAddressFromString(user.MixAddress)
if err != nil {
panic(err)
}
if !checkUser(ctx, req, mix) {
return node.failRequest(ctx, req, "")
}

asset, err := common.SafeReadAssetUntilSufficient(ctx, req.AssetId)
if err != nil || asset == nil {
Expand Down Expand Up @@ -199,10 +208,7 @@ func (node *Node) processSystemCall(ctx context.Context, req *store.Request) ([]
if err != nil {
panic(err)
}
if !slices.ContainsFunc(mix.Members(), func(m string) bool {
return slices.Contains(req.Output.Senders, m)
}) && !common.CheckTestEnvironment(ctx) {
// TODO use better and general authentication without MM api
if !checkUser(ctx, req, mix) {
return node.failRequest(ctx, req, "")
}

Expand Down Expand Up @@ -234,6 +240,15 @@ func (node *Node) processSystemCall(ctx context.Context, req *store.Request) ([]
return node.failRequest(ctx, req, "")
}

old, err := node.store.ReadSystemCallByRequestId(ctx, cid, 0)
if err != nil {
panic(err)
}
if old != nil {
logger.Printf("store.ReadSystemCallByRequestId(%s) => %v", cid, old)
return node.failRequest(ctx, req, "")
}

rb := node.readStorageExtraFromObserver(ctx, *storage)
call, tx, err := node.buildSystemCallFromBytes(ctx, req, cid, rb, false)
if err != nil {
Expand All @@ -244,7 +259,7 @@ func (node *Node) processSystemCall(ctx context.Context, req *store.Request) ([]
call.Public = hex.EncodeToString(user.FingerprintWithPath())
call.SkipPostProcess = skipPostProcess

old, err := node.store.ReadSystemCallByMessage(ctx, call.MessageHash)
old, err = node.store.ReadSystemCallByMessage(ctx, call.MessageHash)
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -276,6 +291,10 @@ func (node *Node) processConfirmNonce(ctx context.Context, req *store.Request) (
}

extra := req.ExtraBytes()
if len(extra) < 1+uuid.Size {
logger.Printf("invalid extra length for confirm nonce: %d", len(extra))
return node.failRequest(ctx, req, "")
}
flag, extra := extra[0], extra[1:]
callId := uuid.Must(uuid.FromBytes(extra[0:16])).String()

Expand Down Expand Up @@ -413,6 +432,15 @@ func (node *Node) processDeployExternalAssetsCall(ctx context.Context, req *stor

var as []*solanaApp.DeployedAsset
extra := req.ExtraBytes()
if len(extra) < 1 {
logger.Printf("invalid extra length for deploy external assets: %d", len(extra))
return node.failRequest(ctx, req, "")
}
assetSize := uuid.Size + solana.PublicKeyLength
if len(extra) != 1+int(extra[0])*assetSize {
logger.Printf("invalid extra length for deploy external assets: %d", len(extra))
return node.failRequest(ctx, req, "")
}
n, extra := extra[0], extra[1:]
offset := 0
for len(as) < int(n) {
Expand Down Expand Up @@ -474,15 +502,27 @@ func (node *Node) processConfirmCall(ctx context.Context, req *store.Request) ([
}

extra := req.ExtraBytes()
if len(extra) < 1 {
logger.Printf("invalid extra length for confirm call: %d", len(extra))
return node.failRequest(ctx, req, "")
}
flag, extra := extra[0], extra[1:]

switch flag {
case FlagConfirmCallSuccess:
if len(extra) < 1 {
logger.Printf("invalid extra length for successful confirm call: %d", len(extra))
return node.failRequest(ctx, req, "")
}
n, extra := int(extra[0]), extra[1:]
if n == 0 || n > 2 {
logger.Printf("invalid length of signature: %d", n)
return node.failRequest(ctx, req, "")
}
if len(extra) < n*solana.SignatureLength {
logger.Printf("invalid signature payload length: %d %d", len(extra), n)
return node.failRequest(ctx, req, "")
}

var calls []*store.SystemCall

Expand Down Expand Up @@ -557,6 +597,10 @@ func (node *Node) processConfirmCall(ctx context.Context, req *store.Request) ([
}
return nil, ""
case FlagConfirmCallFail:
if len(extra) < uuid.Size {
logger.Printf("invalid extra length for failed confirm call: %d", len(extra))
return node.failRequest(ctx, req, "")
}
callId := uuid.Must(uuid.FromBytes(extra[:16])).String()
call, err := node.store.ReadSystemCallByRequestId(ctx, callId, 0)
logger.Printf("store.ReadSystemCallByRequestId(%s) => %v %v", callId, call, err)
Expand All @@ -579,6 +623,10 @@ func (node *Node) processObserverRequestSign(ctx context.Context, req *store.Req
}

extra := req.ExtraBytes()
if len(extra) != uuid.Size {
logger.Printf("invalid extra length for sign request: %d", len(extra))
return node.failRequest(ctx, req, "")
}
callId := uuid.Must(uuid.FromBytes(extra[:16])).String()
call, err := node.store.ReadSystemCallByRequestId(ctx, callId, common.RequestStatePending)
logger.Printf("store.ReadSystemCallByRequestId(%s) => %v %v", callId, call, err)
Expand Down Expand Up @@ -630,6 +678,10 @@ func (node *Node) processObserverCreateDepositCall(ctx context.Context, req *sto
}

extra := req.ExtraBytes()
if len(extra) < solana.PublicKeyLength+solana.SignatureLength {
logger.Printf("invalid extra length for deposit call: %d", len(extra))
return node.failRequest(ctx, req, "")
}
userAddress := solana.PublicKeyFromBytes(extra[:32])
signature := solana.SignatureFromBytes(extra[32:96])

Expand Down Expand Up @@ -858,19 +910,23 @@ func (node *Node) refundAndFailRequest(ctx context.Context, req *store.Request,
}

func (node *Node) failSystemCall(ctx context.Context, req *store.Request, call *store.SystemCall) ([]*mtg.Transaction, string) {
if call == nil {
return node.failRequest(ctx, req, "")
}
extra := req.ExtraBytes()
if len(extra) < 1+uuid.Size {
logger.Printf("invalid extra length for failed system call: %d", len(extra))
return node.failRequest(ctx, req, "")
}
switch call.State {
case common.RequestStatePending, common.RequestStateFailed:
default:
return node.failRequest(ctx, req, "")
}

extra := req.ExtraBytes()
flag, extra := extra[0], extra[1:]

storage := extra[16:]
if call == nil {
panic(req)
}
if flag == FlagConfirmCallSuccess {
storage = nil
}
Expand Down Expand Up @@ -898,7 +954,7 @@ func (node *Node) failSystemCall(ctx context.Context, req *store.Request, call *
}
}

os, _, err := node.GetSystemCallReferenceOutputs(ctx, main.UserIdFromPublicPath(), main.RequestHash, 0)
os, _, err := node.GetSystemCallReferenceOutputs(ctx, main.UserIdFromPublicPath(), main.RequestHash, common.RequestStatePending)
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -1097,3 +1153,11 @@ func (node *Node) confirmBurnRelatedSystemCall(ctx context.Context, req *store.R
}
return txs, ""
}

func checkUser(ctx context.Context, req *store.Request, mix *bot.MixAddress) bool {
if common.CheckTestEnvironment(ctx) {
return true
}

return mix.Threshold == byte(req.Output.SendersThreshold) && bot.HashMembers(mix.Members()) == req.Output.SendersHash
}
10 changes: 9 additions & 1 deletion solana/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,16 @@ func (node *Node) RPCCheckNFT(ctx context.Context, account string) (bool, error)
if err != nil {
return false, err
}
return isNFTAccount(acc)
}

func isNFTAccount(acc *rpc.GetAccountInfoResult) (bool, error) {
data := acc.GetBinary()
if len(data) == 0 {
return false, nil
}
var tm token.Mint
err = bin.NewBinDecoder(acc.Value.Data.GetBinary()).Decode(&tm)
err := bin.NewBinDecoder(data).Decode(&tm)
if err != nil {
return false, fmt.Errorf("solana.NewBinDecoder() => %v", err)
}
Expand Down
Loading
Loading