From 335d59ab1691881e83afbee68acf6b87e834db4b Mon Sep 17 00:00:00 2001 From: Oleg Baranov Date: Sun, 30 Nov 2025 22:18:07 +0400 Subject: [PATCH 1/6] Multiple improvements and new methods --- example/account-state/main.go | 2 +- go.mod | 2 +- go.sum | 4 +-- liteclient/pool.go | 17 +++++++++- ton/api.go | 19 ++++++++++- ton/block.go | 31 +++++++++++++++++ ton/dns/integration_test.go | 2 +- ton/integration_test.go | 20 +++++++++++ ton/retrier.go | 41 ++++++++++++++--------- ton/transactions.go | 63 +++++++++++++++++++++++++++++------ ton/wallet/wallet_test.go | 20 +++++++++++ 11 files changed, 188 insertions(+), 33 deletions(-) diff --git a/example/account-state/main.go b/example/account-state/main.go index 631a5d7e..479143ef 100644 --- a/example/account-state/main.go +++ b/example/account-state/main.go @@ -20,7 +20,7 @@ func main() { return } // initialize ton api lite connection wrapper - api := ton.NewAPIClient(client, ton.ProofCheckPolicyFast).WithRetry() + api := ton.NewAPIClient(client, ton.ProofCheckPolicyFast).WithRetry().WithLSInfoInErrors() // if we want to route all requests to the same node, we can use it ctx := client.StickyContext(context.Background()) diff --git a/go.mod b/go.mod index 0a5e16b6..2917687c 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,5 @@ toolchain go1.24.3 require ( filippo.io/edwards25519 v1.1.0 github.com/xssnick/raptorq v1.3.0 - golang.org/x/crypto v0.42.0 + golang.org/x/crypto v0.45.0 ) diff --git a/go.sum b/go.sum index e8b019ff..c000dc65 100644 --- a/go.sum +++ b/go.sum @@ -2,5 +2,5 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/xssnick/raptorq v1.3.0 h1:3GoaySKMg/i8rbjhIuqjxpTTO2l3Gs2/Gh7k3GAjvGo= github.com/xssnick/raptorq v1.3.0/go.mod h1:kgEVVsZv2hP+IeV7C7985KIFsDdvYq2ARW234SBA9Q4= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= diff --git a/liteclient/pool.go b/liteclient/pool.go index 8670b5e6..85dcc077 100644 --- a/liteclient/pool.go +++ b/liteclient/pool.go @@ -17,6 +17,7 @@ import ( const _StickyCtxKey = "_ton_node_sticky" const _StickyCtxUsedNodesKey = "_ton_used_nodes_sticky" +const CtxLSInfoKey = "_ls_info" type OnDisconnectCallback func(addr, key string) @@ -24,6 +25,10 @@ type ADNLResponse struct { Data tl.Serializable } +type LSInfo struct { + Details string +} + type ADNLRequest struct { QueryID []byte Data any @@ -241,8 +246,18 @@ func (c *ConnectionPool) QueryADNL(ctx context.Context, request tl.Serializable, // wait for response select { case resp := <-ch: + took := time.Since(tm) atomic.AddInt64(&node.weight, 1) - atomic.StoreInt64(&node.lastRespTime, int64(time.Since(tm))) + atomic.StoreInt64(&node.lastRespTime, int64(took)) + + if inf, ok := ctx.Value(CtxLSInfoKey).(*LSInfo); ok && inf != nil { + str := fmt.Sprintf("(%s, took: %d ms)", node.addr, took.Milliseconds()) + if inf.Details != "" { + inf.Details += ", " + str + } else { + inf.Details = str + } + } reflect.ValueOf(result).Elem().Set(reflect.ValueOf(resp.Data)) return nil diff --git a/ton/api.go b/ton/api.go index f2fb64da..5c23fc16 100644 --- a/ton/api.go +++ b/ton/api.go @@ -45,6 +45,8 @@ type ContractExecError struct { type LSError struct { Code int32 `tl:"int"` Text string `tl:"string"` + + Servers string `tl:"-"` } // Deprecated: use APIClientWrapped @@ -56,6 +58,7 @@ type APIClientWrapped interface { GetLibraries(ctx context.Context, list ...[]byte) ([]*cell.Cell, error) LookupBlock(ctx context.Context, workchain int32, shard int64, seqno uint32) (*BlockIDExt, error) GetBlockData(ctx context.Context, block *BlockIDExt) (*tlb.Block, error) + GetBlockHeader(ctx context.Context, block *BlockIDExt) (*tlb.BlockHeader, error) GetBlockTransactionsV2(ctx context.Context, block *BlockIDExt, count uint32, after ...*TransactionID3) ([]TransactionShortInfo, bool, error) GetBlockShardsInfo(ctx context.Context, master *BlockIDExt) ([]*BlockIDExt, error) GetBlockchainConfig(ctx context.Context, block *BlockIDExt, onlyParams ...int32) (*BlockchainConfig, error) @@ -73,10 +76,13 @@ type APIClientWrapped interface { WaitForBlock(seqno uint32) APIClientWrapped WithRetry(maxRetries ...int) APIClientWrapped WithTimeout(timeout time.Duration) APIClientWrapped + WithLSInfoInErrors() APIClientWrapped SetTrustedBlock(block *BlockIDExt) SetTrustedBlockFromConfig(cfg *liteclient.GlobalConfig) FindLastTransactionByInMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) FindLastTransactionByOutMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) + FindLastTransactionByInMsgHashAfterTime(ctx context.Context, addr *address.Address, msgHash []byte, after time.Time) (*tlb.Transaction, error) + FindLastTransactionByOutMsgHashAfterTime(ctx context.Context, addr *address.Address, msgHash []byte, after time.Time) (*tlb.Transaction, error) } type APIClient struct { @@ -146,7 +152,15 @@ func (c *APIClient) WithRetry(maxTries ...int) APIClientWrapped { } return &APIClient{ parent: c, - client: &retryClient{original: c.client, maxRetries: tries}, + client: &retryClient{LiteClient: c.client, maxRetries: tries}, + proofCheckPolicy: c.proofCheckPolicy, + } +} + +func (c *APIClient) WithLSInfoInErrors() APIClientWrapped { + return &APIClient{ + parent: c, + client: &nodeEnricherWrapper{LiteClient: c.client}, proofCheckPolicy: c.proofCheckPolicy, } } @@ -168,6 +182,9 @@ func (c *APIClient) root() *APIClient { } func (e LSError) Error() string { + if e.Servers != "" { + return fmt.Sprintf("lite server error, code %d: [%s] %s", e.Code, e.Servers, e.Text) + } return fmt.Sprintf("lite server error, code %d: %s", e.Code, e.Text) } diff --git a/ton/block.go b/ton/block.go index 112ba863..098f0055 100644 --- a/ton/block.go +++ b/ton/block.go @@ -446,6 +446,37 @@ func (c *APIClient) GetBlockData(ctx context.Context, block *BlockIDExt) (*tlb.B return &bData, nil } +// GetBlockHeader - get block detailed information +func (c *APIClient) GetBlockHeader(ctx context.Context, block *BlockIDExt) (*tlb.BlockHeader, error) { + var resp tl.Serializable + err := c.client.QueryLiteserver(ctx, GetBlockHeader{ID: block}, &resp) + if err != nil { + return nil, err + } + + switch t := resp.(type) { + case BlockHeader: + pl, err := cell.FromBOC(t.HeaderProof) + if err != nil { + return nil, err + } + + pl, err = cell.UnwrapProof(pl, block.RootHash) + if err != nil { + return nil, fmt.Errorf("incorrect proof: %w", err) + } + + var bData tlb.Block + if err = tlb.LoadFromCellAsProof(&bData, pl.BeginParse()); err != nil { + return nil, fmt.Errorf("failed to parse block data proof: %w", err) + } + return &bData.BlockInfo, nil + case LSError: + return nil, t + } + return nil, errUnexpectedResponse(resp) +} + // GetBlockDataAsCell - get block detailed information as a cell func (c *APIClient) GetBlockDataAsCell(ctx context.Context, block *BlockIDExt) (*cell.Cell, error) { var resp tl.Serializable diff --git a/ton/dns/integration_test.go b/ton/dns/integration_test.go index e11d091b..1c3910f7 100644 --- a/ton/dns/integration_test.go +++ b/ton/dns/integration_test.go @@ -19,7 +19,7 @@ var api = func() ton.APIClientWrapped { panic(err) } - return ton.NewAPIClient(client).WithTimeout(5 * time.Second).WithRetry() + return ton.NewAPIClient(client).WithTimeout(5 * time.Second).WithRetry().WithLSInfoInErrors() }() func TestDNSClient_Resolve(t *testing.T) { diff --git a/ton/integration_test.go b/ton/integration_test.go index e7c9db5e..faafbd7a 100644 --- a/ton/integration_test.go +++ b/ton/integration_test.go @@ -121,6 +121,26 @@ func TestAPIClient_GetBlockData(t *testing.T) { // TODO: data check } +func TestAPIClient_GetBlockHeader(t *testing.T) { + ctx := api.Client().StickyContext(context.Background()) + + b, err := api.CurrentMasterchainInfo(ctx) + if err != nil { + t.Fatal("get block err:", err.Error()) + return + } + + hdr, err := api.WaitForBlock(b.SeqNo).GetBlockHeader(ctx, b) + if err != nil { + t.Fatal("Get master block data err:", err.Error()) + return + } + + if hdr.SeqNo != b.SeqNo { + t.Fatal("not eq") + } +} + // commented because public archival LS works too bad to test /*func TestAPIClient_GetOldBlockData(t *testing.T) { client := liteclient.NewConnectionPool() diff --git a/ton/retrier.go b/ton/retrier.go index 9c829556..98cdd93d 100644 --- a/ton/retrier.go +++ b/ton/retrier.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "reflect" "strings" "github.com/xssnick/tonutils-go/liteclient" @@ -12,7 +13,11 @@ import ( type retryClient struct { maxRetries int - original LiteClient + LiteClient +} + +type nodeEnricherWrapper struct { + LiteClient } func (w *retryClient) QueryLiteserver(ctx context.Context, payload tl.Serializable, result tl.Serializable) error { @@ -22,7 +27,7 @@ func (w *retryClient) QueryLiteserver(ctx context.Context, payload tl.Serializab ctxBackup := ctx for { - err := w.original.QueryLiteserver(ctx, payload, result) + err := w.LiteClient.QueryLiteserver(ctx, payload, result) if w.maxRetries > 0 && tries >= w.maxRetries { return err } @@ -39,7 +44,7 @@ func (w *retryClient) QueryLiteserver(ctx context.Context, payload tl.Serializab } // try next node - ctx, err = w.original.StickyContextNextNode(ctx) + ctx, err = w.LiteClient.StickyContextNextNode(ctx) if err != nil { rounds++ if rounds < maxRounds { @@ -61,8 +66,10 @@ func (w *retryClient) QueryLiteserver(ctx context.Context, payload tl.Serializab lsErr.Code == -400 || lsErr.Code == -503 || lsErr.Code == 502 || + lsErr.Code == 228 || + lsErr.Code == 429 || (lsErr.Code == 0 && strings.Contains(lsErr.Text, "Failed to get account state"))) { - if ctx, err = w.original.StickyContextNextNode(ctx); err != nil { // try next node + if ctx, err = w.LiteClient.StickyContextNextNode(ctx); err != nil { // try next node rounds++ if rounds < maxRounds { // try same nodes one more time @@ -80,18 +87,22 @@ func (w *retryClient) QueryLiteserver(ctx context.Context, payload tl.Serializab } } -func (w *retryClient) StickyContext(ctx context.Context) context.Context { - return w.original.StickyContext(ctx) -} +func (w *nodeEnricherWrapper) QueryLiteserver(ctx context.Context, payload tl.Serializable, result tl.Serializable) error { + inf := liteclient.LSInfo{} + ctx = context.WithValue(ctx, liteclient.CtxLSInfoKey, &inf) -func (w *retryClient) StickyNodeID(ctx context.Context) uint32 { - return w.original.StickyNodeID(ctx) -} + err := w.LiteClient.QueryLiteserver(ctx, payload, result) + if err != nil { + return err + } -func (w *retryClient) StickyContextNextNode(ctx context.Context) (context.Context, error) { - return w.original.StickyContextNextNode(ctx) -} + if tmp, ok := result.(*tl.Serializable); ok && tmp != nil { + if lsErr, ok := (*tmp).(LSError); ok { + // enrich with server addr + lsErr.Servers = inf.Details + reflect.ValueOf(result).Elem().Set(reflect.ValueOf(lsErr)) + } + } -func (w *retryClient) StickyContextNextNodeBalanced(ctx context.Context) (context.Context, error) { - return w.original.StickyContextNextNodeBalanced(ctx) + return nil } diff --git a/ton/transactions.go b/ton/transactions.go index f2ccd323..df9dae54 100644 --- a/ton/transactions.go +++ b/ton/transactions.go @@ -298,17 +298,31 @@ func (c *APIClient) SubscribeOnTransactions(workerCtx context.Context, addr *add } } +// Deprecated: use FindLastTransactionByInMsgHashAfterTime, to search by message hash and not body hash // FindLastTransactionByInMsgHash returns last transaction in account where incoming message (payload) hash equal to msgHash. func (c *APIClient) FindLastTransactionByInMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) { - return c.findLastTransactionByHash(ctx, addr, false, msgHash, maxTxNumToScan...) + return c.findLastTransactionByHash(ctx, addr, false, msgHash, time.Time{}, false, maxTxNumToScan...) } +// FindLastTransactionByInMsgHashAfterTime returns last transaction in account where incoming message hash equal to msgHash. +// Checks all account transactions after specified time, use estimated execution time, it allows us to reduce scanning depth +func (c *APIClient) FindLastTransactionByInMsgHashAfterTime(ctx context.Context, addr *address.Address, msgHash []byte, after time.Time) (*tlb.Transaction, error) { + return c.findLastTransactionByHash(ctx, addr, false, msgHash, after, true, 0) +} + +// Deprecated: use FindLastTransactionByOutMsgHashAfterTime, to search by message hash and not body hash // FindLastTransactionByOutMsgHash returns last transaction in account where one of outgoing message (payload) hashes equal to msgHash. func (c *APIClient) FindLastTransactionByOutMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) { - return c.findLastTransactionByHash(ctx, addr, true, msgHash, maxTxNumToScan...) + return c.findLastTransactionByHash(ctx, addr, true, msgHash, time.Time{}, false, maxTxNumToScan...) +} + +// FindLastTransactionByOutMsgHashAfterTime returns last transaction in account where outgoing message hash equal to msgHash. +// Checks all account transactions after specified time, use estimated execution time, it allows us to reduce scanning depth +func (c *APIClient) FindLastTransactionByOutMsgHashAfterTime(ctx context.Context, addr *address.Address, msgHash []byte, after time.Time) (*tlb.Transaction, error) { + return c.findLastTransactionByHash(ctx, addr, true, msgHash, after, true, 0) } -func (c *APIClient) findLastTransactionByHash(ctx context.Context, addr *address.Address, isOut bool, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) { +func (c *APIClient) findLastTransactionByHash(ctx context.Context, addr *address.Address, isOut bool, msgHash []byte, after time.Time, updated bool, maxTxNumToScan ...int) (*tlb.Transaction, error) { limit := 60 if len(maxTxNumToScan) > 0 { limit = maxTxNumToScan[0] @@ -323,7 +337,7 @@ func (c *APIClient) findLastTransactionByHash(ctx context.Context, addr *address if err != nil { return nil, fmt.Errorf("cannot get account: %w", err) } - if !acc.IsActive { // no tx is made from this account + if !acc.IsActive { return nil, fmt.Errorf("account is inactive: %w", ErrTxWasNotFound) } @@ -341,10 +355,15 @@ func (c *APIClient) findLastTransactionByHash(ctx context.Context, addr *address return nil, fmt.Errorf("cannot list transactions: %w", err) } - for i, transaction := range txList { + for i := len(txList) - 1; i >= 0; i-- { + transaction := txList[i] if i == 0 { // get previous of the oldest tx, in case if we need to scan deeper - lastLt, lastHash = txList[0].PrevTxLT, txList[0].PrevTxHash + lastLt, lastHash = transaction.PrevTxLT, transaction.PrevTxHash + } + + if !after.IsZero() && int64(transaction.Now) < after.Unix() { + return nil, ErrTxWasNotFound } if isOut { @@ -358,8 +377,19 @@ func (c *APIClient) findLastTransactionByHash(ctx context.Context, addr *address } for _, m := range list { - if bytes.Equal(m.Msg.Payload().Hash(), msgHash) { - return transaction, nil + if updated { + msgCell, err := tlb.ToCell(m.Msg) + if err != nil { + return nil, fmt.Errorf("cannot convert message to cell: %w", err) + } + + if bytes.Equal(msgCell.Hash(), msgHash) { + return transaction, nil + } + } else { + if bytes.Equal(m.Msg.Payload().Hash(), msgHash) { + return transaction, nil + } } } } else { @@ -372,15 +402,26 @@ func (c *APIClient) findLastTransactionByHash(ctx context.Context, addr *address return transaction, nil } - if bytes.Equal(transaction.IO.In.Msg.Payload().Hash(), msgHash) { - return transaction, nil + if updated { + msgCell, err := tlb.ToCell(transaction.IO.In.Msg) + if err != nil { + return nil, fmt.Errorf("cannot convert message to cell: %w", err) + } + + if bytes.Equal(msgCell.Hash(), msgHash) { + return transaction, nil + } + } else { + if bytes.Equal(transaction.IO.In.Msg.Payload().Hash(), msgHash) { + return transaction, nil + } } } } scanned += 15 - if scanned >= limit { + if limit > 0 && scanned >= limit { return nil, fmt.Errorf("scan limit of %d transactions was reached, %d transactions was checked and hash was not found", limit, scanned) } } diff --git a/ton/wallet/wallet_test.go b/ton/wallet/wallet_test.go index 6af8de5d..7cb9d4f4 100644 --- a/ton/wallet/wallet_test.go +++ b/ton/wallet/wallet_test.go @@ -498,6 +498,26 @@ type WaiterMock struct { MSendExternalMessageWaitTransaction func(ctx context.Context, msg *tlb.ExternalMessage) (*tlb.Transaction, *ton.BlockIDExt, []byte, error) } +func (w WaiterMock) GetBlockHeader(ctx context.Context, block *ton.BlockIDExt) (*tlb.BlockHeader, error) { + //TODO implement me + panic("implement me") +} + +func (w WaiterMock) WithLSInfoInErrors() ton.APIClientWrapped { + //TODO implement me + panic("implement me") +} + +func (w WaiterMock) FindLastTransactionByInMsgHashAfterTime(ctx context.Context, addr *address.Address, msgHash []byte, after time.Time) (*tlb.Transaction, error) { + //TODO implement me + panic("implement me") +} + +func (w WaiterMock) FindLastTransactionByOutMsgHashAfterTime(ctx context.Context, addr *address.Address, msgHash []byte, after time.Time) (*tlb.Transaction, error) { + //TODO implement me + panic("implement me") +} + func (w WaiterMock) FindLastTransactionByInMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) { return w.MFindLastTransactionByInMsgHash(ctx, addr, msgHash, maxTxNumToScan...) } From 7c8c8e6193043b69d981f375a82d0890db31e033 Mon Sep 17 00:00:00 2001 From: Oleg Baranov Date: Tue, 2 Dec 2025 16:23:10 +0400 Subject: [PATCH 2/6] Handled contract not initialized error difference in old ls proxy & Updated toncenter v2 api --- ton/runmethod.go | 5 +++++ toncenter/client-v2.go | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ton/runmethod.go b/ton/runmethod.go index 30c68776..011fabb2 100644 --- a/ton/runmethod.go +++ b/ton/runmethod.go @@ -143,6 +143,11 @@ func (c *APIClient) RunGetMethod(ctx context.Context, blockInfo *BlockIDExt, add return NewExecutionResult(result), nil case LSError: + if t.Code == ErrCodeContractNotInitialized { + return nil, ContractExecError{ + ErrCodeContractNotInitialized, + } + } return nil, t } return nil, errUnexpectedResponse(resp) diff --git a/toncenter/client-v2.go b/toncenter/client-v2.go index 41131cda..9bab3ea6 100644 --- a/toncenter/client-v2.go +++ b/toncenter/client-v2.go @@ -486,11 +486,11 @@ type OutMsgQueueSizesV2Result struct { } func (v *V2) GetOutMsgQueueSizes(ctx context.Context) (*OutMsgQueueSizesV2Result, error) { - return V2GetCall[OutMsgQueueSizesV2Result](ctx, v, "getOutMsgQueueSizes", nil) + return V2GetCall[OutMsgQueueSizesV2Result](ctx, v, "getOutMsgQueueSize", nil) } type TokenDataV2Result struct { - TotalSupply *big.Int `json:"total_supply"` + TotalSupply NanoCoins `json:"total_supply"` Mintable bool `json:"mintable"` AdminAddress *address.Address `json:"admin_address"` JettonContent struct { From 124ac120fe1460164b9eba4e83def10a2978411d Mon Sep 17 00:00:00 2001 From: Oleg Baranov Date: Wed, 3 Dec 2025 14:26:42 +0400 Subject: [PATCH 3/6] Liteclient retrier throw deadline err directly --- ton/retrier.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ton/retrier.go b/ton/retrier.go index 98cdd93d..1e9883d2 100644 --- a/ton/retrier.go +++ b/ton/retrier.go @@ -34,12 +34,7 @@ func (w *retryClient) QueryLiteserver(ctx context.Context, payload tl.Serializab tries++ if err != nil { - if !errors.Is(err, liteclient.ErrADNLReqTimeout) && !errors.Is(err, context.DeadlineExceeded) { - return err - } - - err := ctx.Err() - if err != nil { + if !errors.Is(err, liteclient.ErrADNLReqTimeout) { return err } From 484d59f02b05a67da51269d109e4a427679fe79d Mon Sep 17 00:00:00 2001 From: Oleg Baranov Date: Wed, 4 Mar 2026 17:05:28 +0400 Subject: [PATCH 4/6] Simplex validator set signatures supported & LS queue size methods --- tl/loader.go | 2 +- ton/api.go | 8 +- ton/block.go | 67 ++++++++- ton/integration_test.go | 2 +- ton/liteserver_queue.go | 253 +++++++++++++++++++++++++++++++++ ton/liteserver_queue_test.go | 233 ++++++++++++++++++++++++++++++ ton/proof.go | 152 ++++++++++++++++---- ton/proof_simplex_test.go | 155 ++++++++++++++++++++ ton/wallet/integration_test.go | 6 +- ton/wallet/wallet_test.go | 19 ++- tvm/cell/dict_test.go | 21 +++ 11 files changed, 880 insertions(+), 38 deletions(-) create mode 100644 ton/liteserver_queue.go create mode 100644 ton/liteserver_queue_test.go create mode 100644 ton/proof_simplex_test.go diff --git a/tl/loader.go b/tl/loader.go index 97e654ee..fb90e52f 100644 --- a/tl/loader.go +++ b/tl/loader.go @@ -136,7 +136,7 @@ func getStructInfoReferenceByShortName(name string) *structInfo { func RegisterAllowedGroup(name string, names ...string) { initMx.Lock() // we lock because init() methods in independent packages can be called in parallel defer initMx.Unlock() - + grp := _allowedGroup[name] grp = append(grp, names...) _allowedGroup[name] = grp diff --git a/ton/api.go b/ton/api.go index 5c23fc16..cd2a1e35 100644 --- a/ton/api.go +++ b/ton/api.go @@ -3,11 +3,12 @@ package ton import ( "context" "fmt" - "github.com/xssnick/tonutils-go/liteclient" "reflect" "sync" "time" + "github.com/xssnick/tonutils-go/liteclient" + "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tl" "github.com/xssnick/tonutils-go/tlb" @@ -83,6 +84,11 @@ type APIClientWrapped interface { FindLastTransactionByOutMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) FindLastTransactionByInMsgHashAfterTime(ctx context.Context, addr *address.Address, msgHash []byte, after time.Time) (*tlb.Transaction, error) FindLastTransactionByOutMsgHashAfterTime(ctx context.Context, addr *address.Address, msgHash []byte, after time.Time) (*tlb.Transaction, error) + + GetOutMsgQueueSizes(ctx context.Context, wc *int32, shard *int64) (*OutMsgQueueSizes, error) + GetBlockOutMsgQueueSize(ctx context.Context, block *BlockIDExt) (*BlockOutMsgQueueSize, error) + GetDispatchQueueInfo(ctx context.Context, block *BlockIDExt, afterAddr *address.Address, maxAccounts int) (*DispatchQueueInfo, error) + GetDispatchQueueMessages(ctx context.Context, block *BlockIDExt, addr *address.Address, afterLT uint64, maxMessages int, options ...func(*GetDispatchQueueMessages)) (*DispatchQueueMessages, error) } type APIClient struct { diff --git a/ton/block.go b/ton/block.go index 098f0055..5a4f8298 100644 --- a/ton/block.go +++ b/ton/block.go @@ -49,8 +49,17 @@ func init() { tl.Register(BlockLinkBackward{}, "liteServer.blockLinkBack to_key_block:Bool from:tonNode.blockIdExt to:tonNode.blockIdExt dest_proof:bytes proof:bytes state_proof:bytes = liteServer.BlockLink") tl.Register(BlockLinkForward{}, "liteServer.blockLinkForward to_key_block:Bool from:tonNode.blockIdExt to:tonNode.blockIdExt dest_proof:bytes config_proof:bytes signatures:liteServer.SignatureSet = liteServer.BlockLink") tl.Register(SignatureSet{}, "liteServer.signatureSet validator_set_hash:int catchain_seqno:int signatures:(vector liteServer.signature) = liteServer.SignatureSet") + tl.Register(SignatureSetOrdinary{}, "liteServer.signatureSet.ordinary#f644a6e6 validator_set_hash:int catchain_seqno:int signatures:(vector liteServer.signature) = liteServer.SignatureSet") + tl.Register(SignatureSetSimplex{}, "liteServer.signatureSet.simplex cc_seqno:int validator_set_hash:int signatures:(vector liteServer.signature) session_id:int256 slot:int candidate:bytes = liteServer.SignatureSet") tl.Register(Signature{}, "liteServer.signature node_id_short:int256 signature:bytes = liteServer.Signature") tl.Register(BlockID{}, "ton.blockId root_cell_hash:int256 file_hash:int256 = ton.BlockId") + tl.Register(ConsensusDataToSign{}, "consensus.dataToSign session_id:int256 data:bytes = consensus.DataToSign") + tl.Register(ConsensusCandidateID{}, "consensus.candidateId slot:int hash:int256 = consensus.CandidateId") + tl.Register(ConsensusSimplexFinalizeVote{}, "consensus.simplex.finalizeVote id:consensus.CandidateId = consensus.simplex.UnsignedVote") + tl.Register(ConsensusCandidateParent{}, "consensus.candidateParent id:consensus.CandidateId = consensus.CandidateParent") + tl.Register(ConsensusCandidateWithoutParents{}, "consensus.candidateWithoutParents = consensus.CandidateParent") + tl.Register(ConsensusCandidateHashDataOrdinary{}, "consensus.candidateHashDataOrdinary block:tonNode.blockIdExt collated_file_hash:int256 parent:consensus.CandidateParent = consensus.CandidateHashData") + tl.Register(ConsensusCandidateHashDataEmpty{}, "consensus.candidateHashDataEmpty block:tonNode.blockIdExt parent:consensus.candidateId = consensus.CandidateHashData") tl.Register(GetVersion{}, "liteServer.getVersion = liteServer.Version") tl.Register(Version{}, "liteServer.version mode:# version:int capabilities:long now:int = liteServer.Version") @@ -118,12 +127,12 @@ type BlockLinkBackward struct { } type BlockLinkForward struct { - ToKeyBlock bool `tl:"bool"` - From *BlockIDExt `tl:"struct"` - To *BlockIDExt `tl:"struct"` - DestProof []byte `tl:"bytes"` - ConfigProof []byte `tl:"bytes"` - SignatureSet *SignatureSet `tl:"struct boxed"` + ToKeyBlock bool `tl:"bool"` + From *BlockIDExt `tl:"struct"` + To *BlockIDExt `tl:"struct"` + DestProof []byte `tl:"bytes"` + ConfigProof []byte `tl:"bytes"` + SignatureSet any `tl:"struct boxed [liteServer.signatureSet,liteServer.signatureSet.ordinary,liteServer.signatureSet.simplex]"` } type SignatureSet struct { @@ -132,11 +141,57 @@ type SignatureSet struct { Signatures []Signature `tl:"vector struct"` } +type SignatureSetOrdinary struct { + ValidatorSetHash int32 `tl:"int"` + CatchainSeqno int32 `tl:"int"` + Signatures []Signature `tl:"vector struct"` +} + +type SignatureSetSimplex struct { + CCSeqno int32 `tl:"int"` + ValidatorSetHash int32 `tl:"int"` + Signatures []Signature `tl:"vector struct"` + SessionID []byte `tl:"int256"` + Slot int32 `tl:"int"` + Candidate []byte `tl:"bytes"` +} + type Signature struct { NodeIDShort []byte `tl:"int256"` Signature []byte `tl:"bytes"` } +type ConsensusDataToSign struct { + SessionID []byte `tl:"int256"` + Data []byte `tl:"bytes"` +} + +type ConsensusCandidateID struct { + Slot int32 `tl:"int"` + Hash []byte `tl:"int256"` +} + +type ConsensusSimplexFinalizeVote struct { + ID any `tl:"struct boxed [consensus.candidateId]"` +} + +type ConsensusCandidateParent struct { + ID any `tl:"struct boxed [consensus.candidateId]"` +} + +type ConsensusCandidateWithoutParents struct{} + +type ConsensusCandidateHashDataOrdinary struct { + Block BlockIDExt `tl:"struct"` + CollatedFileHash []byte `tl:"int256"` + Parent any `tl:"struct boxed [consensus.candidateParent,consensus.candidateWithoutParents]"` +} + +type ConsensusCandidateHashDataEmpty struct { + Block BlockIDExt `tl:"struct"` + Parent ConsensusCandidateID `tl:"struct"` +} + type Object struct{} type True struct{} diff --git a/ton/integration_test.go b/ton/integration_test.go index faafbd7a..83483396 100644 --- a/ton/integration_test.go +++ b/ton/integration_test.go @@ -24,7 +24,7 @@ var apiTestNet = func() APIClientWrapped { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - err := client.AddConnectionsFromConfigUrl(ctx, "https://ton-blockchain.github.io/testnet-global.config.json") + err := client.AddConnection(ctx, "109.236.80.69:49913", "AxFZRHVD1qIO9Fyva52P4vC3tRvk8ac1KKOG0c6IVio=") if err != nil { panic(err) } diff --git a/ton/liteserver_queue.go b/ton/liteserver_queue.go new file mode 100644 index 00000000..7d02b79b --- /dev/null +++ b/ton/liteserver_queue.go @@ -0,0 +1,253 @@ +package ton + +import ( + "context" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tl" +) + +func init() { + tl.Register(OutMsgQueueSizes{}, "liteServer.outMsgQueueSizes shards:(vector liteServer.outMsgQueueSize) ext_msg_queue_size_limit:int = liteServer.OutMsgQueueSizes") + tl.Register(OutMsgQueueSize{}, "liteServer.outMsgQueueSize id:tonNode.blockIdExt size:int = liteServer.OutMsgQueueSize") + tl.Register(BlockOutMsgQueueSize{}, "liteServer.blockOutMsgQueueSize mode:# id:tonNode.blockIdExt size:long proof:mode.0?bytes = liteServer.BlockOutMsgQueueSize") + tl.Register(DispatchQueueInfo{}, "liteServer.dispatchQueueInfo mode:# id:tonNode.blockIdExt account_dispatch_queues:(vector liteServer.accountDispatchQueueInfo) complete:Bool proof:mode.0?bytes = liteServer.DispatchQueueInfo") + tl.Register(AccountDispatchQueueInfo{}, "liteServer.accountDispatchQueueInfo addr:int256 size:long min_lt:long max_lt:long = liteServer.AccountDispatchQueueInfo") + tl.Register(DispatchQueueMessages{}, "liteServer.dispatchQueueMessages mode:# id:tonNode.blockIdExt messages:(vector liteServer.dispatchQueueMessage) complete:Bool proof:mode.0?bytes messages_boc:mode.2?bytes = liteServer.DispatchQueueMessages") + tl.Register(DispatchQueueMessage{}, "liteServer.dispatchQueueMessage addr:int256 lt:long hash:int256 metadata:liteServer.transactionMetadata = liteServer.DispatchQueueMessage") + tl.Register(TransactionMetadata{}, "liteServer.transactionMetadata mode:# depth:int initiator:liteServer.accountId initiator_lt:long = liteServer.TransactionMetadata") + + tl.Register(GetOutMsgQueueSizes{}, "liteServer.getOutMsgQueueSizes mode:# wc:mode.0?int shard:mode.0?long = liteServer.OutMsgQueueSizes") + tl.Register(GetBlockOutMsgQueueSize{}, "liteServer.getBlockOutMsgQueueSize mode:# id:tonNode.blockIdExt want_proof:mode.0?true = liteServer.BlockOutMsgQueueSize") + tl.Register(GetDispatchQueueInfo{}, "liteServer.getDispatchQueueInfo mode:# id:tonNode.blockIdExt after_addr:mode.1?int256 max_accounts:int want_proof:mode.0?true = liteServer.DispatchQueueInfo") + tl.Register(GetDispatchQueueMessages{}, "liteServer.getDispatchQueueMessages mode:# id:tonNode.blockIdExt addr:int256 after_lt:long max_messages:int want_proof:mode.0?true one_account:mode.1?true messages_boc:mode.2?true = liteServer.DispatchQueueMessages") +} + +type OutMsgQueueSizes struct { + Shards []OutMsgQueueSize `tl:"vector struct"` + ExtMsgQueueSizeLimit int32 `tl:"int"` +} + +type OutMsgQueueSize struct { + ID *BlockIDExt `tl:"struct"` + Size int32 `tl:"int"` +} + +type BlockOutMsgQueueSize struct { + Mode uint32 `tl:"flags"` + ID *BlockIDExt `tl:"struct"` + Size int64 `tl:"long"` + Proof []byte `tl:"?0 bytes"` +} + +type DispatchQueueInfo struct { + Mode uint32 `tl:"flags"` + ID *BlockIDExt `tl:"struct"` + AccountDispatchQueues []AccountDispatchQueueInfo `tl:"vector struct"` + Complete bool `tl:"bool"` + Proof []byte `tl:"?0 bytes"` +} + +type AccountDispatchQueueInfo struct { + Addr []byte `tl:"int256"` + Size int64 `tl:"long"` + MinLT uint64 `tl:"long"` + MaxLT uint64 `tl:"long"` +} + +type DispatchQueueMessages struct { + Mode uint32 `tl:"flags"` + ID *BlockIDExt `tl:"struct"` + Messages []DispatchQueueMessage `tl:"vector struct"` + Complete bool `tl:"bool"` + Proof []byte `tl:"?0 bytes"` + MessagesBOC []byte `tl:"?2 bytes"` +} + +type DispatchQueueMessage struct { + Addr []byte `tl:"int256"` + LT uint64 `tl:"long"` + Hash []byte `tl:"int256"` + Metadata TransactionMetadata `tl:"struct"` +} + +type TransactionMetadata struct { + Mode uint32 `tl:"flags"` + Depth int32 `tl:"int"` + Initiator AccountId `tl:"struct"` + InitiatorLT uint64 `tl:"long"` +} + +type AccountId struct { + Workchain int32 `tl:"int"` + ID []byte `tl:"int256"` +} + +// Requests + +type GetOutMsgQueueSizes struct { + Mode uint32 `tl:"flags"` + WC int32 `tl:"?0 int"` + Shard int64 `tl:"?0 long"` +} + +type GetBlockOutMsgQueueSize struct { + Mode uint32 `tl:"flags"` + ID *BlockIDExt `tl:"struct"` + WantProof *True `tl:"?0 struct"` +} + +type GetDispatchQueueInfo struct { + Mode uint32 `tl:"flags"` + ID *BlockIDExt `tl:"struct"` + AfterAddr []byte `tl:"?1 int256"` + MaxAccounts int32 `tl:"int"` + WantProof *True `tl:"?0 struct"` +} + +type GetDispatchQueueMessages struct { + Mode uint32 `tl:"flags"` + ID *BlockIDExt `tl:"struct"` + Addr []byte `tl:"int256"` + AfterLT uint64 `tl:"long"` + MaxMessages int32 `tl:"int"` + WantProof *True `tl:"?0 struct"` + OneAccount *True `tl:"?1 struct"` + MessagesBOC *True `tl:"?2 struct"` +} + +func (c *APIClient) GetOutMsgQueueSizes(ctx context.Context, wc *int32, shard *int64) (*OutMsgQueueSizes, error) { + req := GetOutMsgQueueSizes{} + if wc != nil && shard != nil { + req.Mode = 1 + req.WC = *wc + req.Shard = *shard + } + + var resp tl.Serializable + err := c.client.QueryLiteserver(ctx, req, &resp) + if err != nil { + return nil, err + } + + switch t := resp.(type) { + case OutMsgQueueSizes: + return &t, nil + case LSError: + return nil, t + } + return nil, errUnexpectedResponse(resp) +} + +func (c *APIClient) GetBlockOutMsgQueueSize(ctx context.Context, block *BlockIDExt) (*BlockOutMsgQueueSize, error) { + // TODO: support proofs + req := GetBlockOutMsgQueueSize{ + ID: block, + } + + var resp tl.Serializable + err := c.client.QueryLiteserver(ctx, req, &resp) + if err != nil { + return nil, err + } + + switch t := resp.(type) { + case BlockOutMsgQueueSize: + return &t, nil + case LSError: + return nil, t + } + return nil, errUnexpectedResponse(resp) +} + +func (c *APIClient) GetDispatchQueueInfo(ctx context.Context, block *BlockIDExt, afterAddr *address.Address, maxAccounts int) (*DispatchQueueInfo, error) { + // TODO: support proofs + req := GetDispatchQueueInfo{ + ID: block, + MaxAccounts: int32(maxAccounts), + } + + if afterAddr != nil { + req.Mode |= 1 << 1 + req.AfterAddr = afterAddr.Data() + } + + var resp tl.Serializable + err := c.client.QueryLiteserver(ctx, req, &resp) + if err != nil { + return nil, err + } + + switch t := resp.(type) { + case DispatchQueueInfo: + return &t, nil + case LSError: + return nil, t + } + return nil, errUnexpectedResponse(resp) +} + +func (c *APIClient) GetDispatchQueueMessages(ctx context.Context, block *BlockIDExt, addr *address.Address, afterLT uint64, maxMessages int, options ...func(*GetDispatchQueueMessages)) (*DispatchQueueMessages, error) { + // TODO: support proofs + req := GetDispatchQueueMessages{ + ID: block, + Addr: addr.Data(), + AfterLT: afterLT, + MaxMessages: int32(maxMessages), + } + + for _, opt := range options { + opt(&req) + } + + var resp tl.Serializable + err := c.client.QueryLiteserver(ctx, req, &resp) + if err != nil { + return nil, err + } + + switch t := resp.(type) { + case DispatchQueueMessages: + return &t, nil + case LSError: + return nil, t + } + return nil, errUnexpectedResponse(resp) +} + +func (m *AccountDispatchQueueInfo) Address() *address.Address { + addr := address.NewAddress(0, 0, m.Addr) + return addr +} + +func (m *DispatchQueueMessage) Address() *address.Address { + addr := address.NewAddress(0, 0, m.Addr) + return addr +} + +func (m *TransactionMetadata) InitiatorAddress() *address.Address { + addr := address.NewAddress(0, byte(m.Initiator.Workchain), m.Initiator.ID) + return addr +} + +func (m *DispatchQueueMessages) GetTotalMessagesCount() int { + // If messages BOC is present, we need to parse it to get exact count if it differs from []Messages + // For now just return slice length + return len(m.Messages) +} + +// Options for GetDispatchQueueMessages + +func WithDispatchQueueMessagesBOC() func(*GetDispatchQueueMessages) { + return func(req *GetDispatchQueueMessages) { + req.Mode |= 1 << 2 + req.MessagesBOC = &True{} + } +} + +func WithDispatchQueueOneAccount() func(*GetDispatchQueueMessages) { + return func(req *GetDispatchQueueMessages) { + req.Mode |= 1 << 1 + req.OneAccount = &True{} + } +} diff --git a/ton/liteserver_queue_test.go b/ton/liteserver_queue_test.go new file mode 100644 index 00000000..0048120c --- /dev/null +++ b/ton/liteserver_queue_test.go @@ -0,0 +1,233 @@ +package ton + +import ( + "context" + "reflect" + "testing" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tl" +) + +type ValidationMock struct { + CheckQuery func(payload tl.Serializable) error + Response tl.Serializable +} + +func (m *ValidationMock) QueryLiteserver(ctx context.Context, payload tl.Serializable, result tl.Serializable) error { + if err := m.CheckQuery(payload); err != nil { + return err + } + // reflect to set result + reflect.ValueOf(result).Elem().Set(reflect.ValueOf(m.Response)) + return nil +} + +func (m *ValidationMock) StickyContext(ctx context.Context) context.Context { + return ctx +} + +func (m *ValidationMock) StickyContextNextNode(ctx context.Context) (context.Context, error) { + return ctx, nil +} + +func (m *ValidationMock) StickyContextNextNodeBalanced(ctx context.Context) (context.Context, error) { + return ctx, nil +} + +func (m *ValidationMock) StickyNodeID(ctx context.Context) uint32 { + return 0 +} + +func TestGetOutMsgQueueSizes(t *testing.T) { + mock := &ValidationMock{ + Response: OutMsgQueueSizes{ + ExtMsgQueueSizeLimit: 100, + Shards: []OutMsgQueueSize{ + { + ID: &BlockIDExt{Workchain: 0, Shard: -9223372036854775808, SeqNo: 100}, + Size: 50, + }, + }, + }, + CheckQuery: func(payload tl.Serializable) error { + req, ok := payload.(GetOutMsgQueueSizes) + if !ok { + t.Fatalf("unexpected request type: %T", payload) + } + if req.Mode != 0 { + t.Errorf("expected mode 0, got %d", req.Mode) + } + return nil + }, + } + + client := NewAPIClient(mock) + res, err := client.GetOutMsgQueueSizes(context.Background(), nil, nil) + if err != nil { + t.Fatal(err) + } + + if res.ExtMsgQueueSizeLimit != 100 { + t.Errorf("expected limit 100, got %d", res.ExtMsgQueueSizeLimit) + } + if len(res.Shards) != 1 { + t.Errorf("expected 1 shard, got %d", len(res.Shards)) + } +} + +func TestGetOutMsgQueueSizes_SpecificMsg(t *testing.T) { + wc := int32(-1) + shard := int64(-9223372036854775808) + + mock := &ValidationMock{ + Response: OutMsgQueueSizes{}, + CheckQuery: func(payload tl.Serializable) error { + req, ok := payload.(GetOutMsgQueueSizes) + if !ok { + t.Fatalf("unexpected request type: %T", payload) + } + if req.Mode != 1 { + t.Errorf("expected mode 1, got %d", req.Mode) + } + if req.WC != wc || req.Shard != shard { + t.Errorf("unexpected wc/shard: %d/%d", req.WC, req.Shard) + } + return nil + }, + } + + client := NewAPIClient(mock) + _, err := client.GetOutMsgQueueSizes(context.Background(), &wc, &shard) + if err != nil { + t.Fatal(err) + } +} + +func TestGetBlockOutMsgQueueSize(t *testing.T) { + block := &BlockIDExt{ + Workchain: 0, + Shard: -9223372036854775808, + SeqNo: 12345, + } + + mock := &ValidationMock{ + Response: BlockOutMsgQueueSize{ + ID: block, + Size: 1024, + }, + CheckQuery: func(payload tl.Serializable) error { + req, ok := payload.(GetBlockOutMsgQueueSize) + if !ok { + t.Fatalf("unexpected request type: %T", payload) + } + if !reflect.DeepEqual(req.ID, block) { + t.Errorf("unexpected block id in request") + } + return nil + }, + } + + client := NewAPIClient(mock) + res, err := client.GetBlockOutMsgQueueSize(context.Background(), block) + if err != nil { + t.Fatal(err) + } + + if res.Size != 1024 { + t.Errorf("expected size 1024, got %d", res.Size) + } +} + +func TestGetDispatchQueueInfo(t *testing.T) { + block := &BlockIDExt{Workchain: 0, SeqNo: 100} + addr := address.MustParseAddr("EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c") + + mock := &ValidationMock{ + Response: DispatchQueueInfo{ + ID: block, + Complete: true, + AccountDispatchQueues: []AccountDispatchQueueInfo{ + { + Addr: addr.Data(), + Size: 500, + }, + }, + }, + CheckQuery: func(payload tl.Serializable) error { + req, ok := payload.(GetDispatchQueueInfo) + if !ok { + t.Fatalf("unexpected request type: %T", payload) + } + if req.MaxAccounts != 10 { + t.Errorf("expected max accounts 10, got %d", req.MaxAccounts) + } + if req.Mode&2 == 0 { // 1 << 1 + t.Error("expected after addr flag") + } + return nil + }, + } + + client := NewAPIClient(mock) + res, err := client.GetDispatchQueueInfo(context.Background(), block, addr, 10) + if err != nil { + t.Fatal(err) + } + + if !res.Complete { + t.Error("expected complete") + } +} + +func TestGetDispatchQueueMessages(t *testing.T) { + block := &BlockIDExt{Workchain: 0, SeqNo: 100} + addr := address.MustParseAddr("EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c") + + mock := &ValidationMock{ + Response: DispatchQueueMessages{ + ID: block, + Messages: []DispatchQueueMessage{ + { + LT: 1000, + Metadata: TransactionMetadata{ + Depth: 1, + Initiator: AccountId{ + Workchain: 0, + ID: addr.Data(), + }, + }, + }, + }, + }, + CheckQuery: func(payload tl.Serializable) error { + req, ok := payload.(GetDispatchQueueMessages) + if !ok { + t.Fatalf("unexpected request type: %T", payload) + } + if req.AfterLT != 500 { + t.Errorf("expected after lt 500, got %d", req.AfterLT) + } + if req.Mode&4 == 0 { // 1 << 2 + t.Error("expected messages boc flag") + } + return nil + }, + } + + client := NewAPIClient(mock) + // Using WithDispatchQueueMessagesBOC option + res, err := client.GetDispatchQueueMessages(context.Background(), block, addr, 500, 5, WithDispatchQueueMessagesBOC()) + if err != nil { + t.Fatal(err) + } + + if len(res.Messages) != 1 { + t.Errorf("expected 1 message, got %d", len(res.Messages)) + } + + // Check helper methods + if res.Messages[0].Metadata.InitiatorAddress().String() != addr.String() { + t.Error("initiator address mismatch") + } +} diff --git a/ton/proof.go b/ton/proof.go index 3e605d46..9469e6ef 100644 --- a/ton/proof.go +++ b/ton/proof.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/ed25519" + "crypto/sha256" "encoding/hex" "errors" "fmt" @@ -264,7 +265,7 @@ func CheckBackwardBlockProof(from, to *BlockIDExt, toKey bool, stateProof, destP return nil } -func CheckForwardBlockProof(from, to *BlockIDExt, toKey bool, configProof, destProof *cell.Cell, signatures *SignatureSet) error { +func CheckForwardBlockProof(from, to *BlockIDExt, toKey bool, configProof, destProof *cell.Cell, sigSet any) error { if from.Workchain != address.MasterchainID || to.Workchain != address.MasterchainID { return fmt.Errorf("both blocks should be from masterchain") } @@ -282,14 +283,6 @@ func CheckForwardBlockProof(from, to *BlockIDExt, toKey bool, configProof, destP return fmt.Errorf("target block type not matches requested") } - if toBlock.BlockInfo.GenValidatorListHashShort != uint32(signatures.ValidatorSetHash) { - return fmt.Errorf("incorrect validator set hash") - } - - if toBlock.BlockInfo.GenCatchainSeqno != uint32(signatures.CatchainSeqno) { - return fmt.Errorf("incorrect catchain seqno") - } - if toBlock.BlockInfo.SeqNo <= from.SeqNo { return fmt.Errorf("invalid target block seqno") } @@ -330,8 +323,45 @@ func CheckForwardBlockProof(from, to *BlockIDExt, toKey bool, configProof, destP return fmt.Errorf("failed to verify and get main block validators: %w", err) } - if err = CheckBlockSignatures(to, signatures, validators); err != nil { - return fmt.Errorf("failed to check validators signatures: %w", err) + var valSetHash uint32 + var valSetSeqno uint32 + var signatures []Signature + var simplexSet *SignatureSetSimplex + switch t := sigSet.(type) { + case SignatureSet: + valSetSeqno = uint32(t.CatchainSeqno) + valSetHash = uint32(t.ValidatorSetHash) + signatures = t.Signatures + case SignatureSetOrdinary: + valSetSeqno = uint32(t.CatchainSeqno) + valSetHash = uint32(t.ValidatorSetHash) + signatures = t.Signatures + case SignatureSetSimplex: + valSetSeqno = uint32(t.CCSeqno) + valSetHash = uint32(t.ValidatorSetHash) + signatures = t.Signatures + simplexSet = &t + default: + return fmt.Errorf("unsupported signature set type %T", sigSet) + } + + if signatures != nil { + if toBlock.BlockInfo.GenValidatorListHashShort != valSetHash { + return fmt.Errorf("incorrect validator set hash") + } + + if toBlock.BlockInfo.GenCatchainSeqno != valSetSeqno { + return fmt.Errorf("incorrect catchain seqno") + } + + if simplexSet != nil { + err = CheckBlockSignaturesSimplex(to, valSetSeqno, valSetHash, simplexSet.SessionID, simplexSet.Slot, simplexSet.Candidate, signatures, validators) + } else { + err = CheckBlockSignatures(to, valSetSeqno, valSetHash, signatures, validators) + } + if err != nil { + return fmt.Errorf("failed to check validators signatures: %w", err) + } } return nil @@ -436,17 +466,94 @@ func GetMainValidators(block *BlockIDExt, catConfig tlb.CatchainConfig, validato return validators, nil } -func CheckBlockSignatures(block *BlockIDExt, sigs *SignatureSet, validators []*tlb.ValidatorAddr) error { - if len(sigs.Signatures) == 0 || len(validators) == 0 { +func CheckBlockSignatures(block *BlockIDExt, chainSeqno, setHash uint32, sigs []Signature, validators []*tlb.ValidatorAddr) error { + blockIDBytes, err := tl.Serialize(BlockID{RootHash: block.RootHash, FileHash: block.FileHash}, true) + if err != nil { + return fmt.Errorf("failed to serialize block id: %w", err) + } + + return checkBlockSignaturesPayload(blockIDBytes, chainSeqno, setHash, sigs, validators) +} + +func CheckBlockSignaturesSimplex(block *BlockIDExt, chainSeqno, setHash uint32, sessionID []byte, slot int32, candidate []byte, sigs []Signature, validators []*tlb.ValidatorAddr) error { + toSign, err := buildSimplexToSignPayload(block, sessionID, slot, candidate) + if err != nil { + return err + } + return checkBlockSignaturesPayload(toSign, chainSeqno, setHash, sigs, validators) +} + +func buildSimplexToSignPayload(block *BlockIDExt, sessionID []byte, slot int32, candidate []byte) ([]byte, error) { + if len(sessionID) != 32 { + return nil, fmt.Errorf("invalid simplex session id len %d", len(sessionID)) + } + if len(candidate) == 0 { + return nil, fmt.Errorf("empty simplex candidate") + } + + candidateBlock, err := parseSimplexCandidateBlock(candidate) + if err != nil { + return nil, fmt.Errorf("failed to parse simplex candidate: %w", err) + } + if !candidateBlock.Equals(block) { + return nil, fmt.Errorf("simplex candidate block id mismatch") + } + + candidateHash := sha256.Sum256(candidate) + voteData, err := tl.Serialize(ConsensusSimplexFinalizeVote{ + ID: ConsensusCandidateID{ + Slot: slot, + Hash: candidateHash[:], + }, + }, true) + if err != nil { + return nil, fmt.Errorf("failed to serialize simplex finalize vote: %w", err) + } + + toSign, err := tl.Serialize(ConsensusDataToSign{ + SessionID: sessionID, + Data: voteData, + }, true) + if err != nil { + return nil, fmt.Errorf("failed to serialize simplex data to sign: %w", err) + } + + return toSign, nil +} + +func parseSimplexCandidateBlock(candidate []byte) (*BlockIDExt, error) { + var ordinary ConsensusCandidateHashDataOrdinary + left, errOrdinary := tl.Parse(&ordinary, candidate, true) + if errOrdinary == nil { + if len(left) > 0 { + return nil, fmt.Errorf("unexpected trailing bytes in ordinary candidate") + } + return &ordinary.Block, nil + } + + var empty ConsensusCandidateHashDataEmpty + left, errEmpty := tl.Parse(&empty, candidate, true) + if errEmpty == nil { + if len(left) > 0 { + return nil, fmt.Errorf("unexpected trailing bytes in empty candidate") + } + return &empty.Block, nil + } + + return nil, fmt.Errorf("unsupported candidate type: ordinary parse failed: %v; empty parse failed: %v", errOrdinary, errEmpty) +} + +func checkBlockSignaturesPayload(toSign []byte, chainSeqno, setHash uint32, sigs []Signature, validators []*tlb.ValidatorAddr) error { + if len(sigs) == 0 || len(validators) == 0 { return fmt.Errorf("zero signatures or validators") } - setHash, err := calcValidatorSetHash(uint32(sigs.CatchainSeqno), validators) + calcedSetHash, err := calcValidatorSetHash(chainSeqno, validators) if err != nil { return fmt.Errorf("failed to calc validator set hash: %w", err) } - if setHash != uint32(sigs.ValidatorSetHash) { + if setHash != calcedSetHash { return fmt.Errorf("incorrect validator set hash") } @@ -462,17 +569,12 @@ func CheckBlockSignatures(block *BlockIDExt, sigs *SignatureSet, validators []*t validatorsMap[string(kid)] = v } - blockIDBytes, err := tl.Serialize(BlockID{RootHash: block.RootHash, FileHash: block.FileHash}, true) - if err != nil { - return fmt.Errorf("failed to serialize block id: %w", err) - } - - sort.Slice(sigs.Signatures, func(i, j int) bool { - return string(sigs.Signatures[i].NodeIDShort) < string(sigs.Signatures[j].NodeIDShort) + sort.Slice(sigs, func(i, j int) bool { + return string(sigs[i].NodeIDShort) < string(sigs[j].NodeIDShort) }) - for i, sig := range sigs.Signatures { - if i > 0 && string(sigs.Signatures[i-1].NodeIDShort) == string(sig.NodeIDShort) { + for i, sig := range sigs { + if i > 0 && string(sigs[i-1].NodeIDShort) == string(sig.NodeIDShort) { return fmt.Errorf("duplicated node signature") } @@ -481,7 +583,7 @@ func CheckBlockSignatures(block *BlockIDExt, sigs *SignatureSet, validators []*t return fmt.Errorf("signature of unknown validator %s", hex.EncodeToString(sig.NodeIDShort)) } - if !ed25519.Verify(v.PublicKey.Key, blockIDBytes, sig.Signature) { + if !ed25519.Verify(v.PublicKey.Key, toSign, sig.Signature) { return fmt.Errorf("incorrect signature of validator %s", hex.EncodeToString(sig.NodeIDShort)) } signedWeight += v.Weight diff --git a/ton/proof_simplex_test.go b/ton/proof_simplex_test.go new file mode 100644 index 00000000..03fcf8f8 --- /dev/null +++ b/ton/proof_simplex_test.go @@ -0,0 +1,155 @@ +package ton + +import ( + "bytes" + "crypto/ed25519" + "strings" + "testing" + + "github.com/xssnick/tonutils-go/adnl/keys" + "github.com/xssnick/tonutils-go/tl" + "github.com/xssnick/tonutils-go/tlb" +) + +func makeSimplexValidator(t *testing.T, seedByte byte, weight uint64) (*tlb.ValidatorAddr, ed25519.PrivateKey, []byte) { + t.Helper() + + seed := bytes.Repeat([]byte{seedByte}, ed25519.SeedSize) + priv := ed25519.NewKeyFromSeed(seed) + pub := priv.Public().(ed25519.PublicKey) + + nodeID, err := tl.Hash(keys.PublicKeyED25519{Key: pub}) + if err != nil { + t.Fatalf("failed to build node id short: %v", err) + } + + return &tlb.ValidatorAddr{ + PublicKey: tlb.SigPubKeyED25519{Key: pub}, + Weight: weight, + ADNLAddr: bytes.Repeat([]byte{seedByte + 1}, 32), + }, priv, nodeID +} + +func buildSimplexCandidate(t *testing.T, block *BlockIDExt) []byte { + t.Helper() + + candidate, err := tl.Serialize(ConsensusCandidateHashDataOrdinary{ + Block: *block, + CollatedFileHash: bytes.Repeat([]byte{0x77}, 32), + Parent: ConsensusCandidateWithoutParents{}, + }, true) + if err != nil { + t.Fatalf("failed to build simplex candidate: %v", err) + } + return candidate +} + +func TestCheckBlockSignaturesSimplex_Valid(t *testing.T) { + v1, pk1, id1 := makeSimplexValidator(t, 0x11, 40) + v2, pk2, id2 := makeSimplexValidator(t, 0x22, 35) + v3, _, _ := makeSimplexValidator(t, 0x33, 25) + validators := []*tlb.ValidatorAddr{v1, v2, v3} + + const ccSeqno uint32 = 17 + setHash, err := calcValidatorSetHash(ccSeqno, validators) + if err != nil { + t.Fatalf("failed to calc validator set hash: %v", err) + } + + block := &BlockIDExt{ + Workchain: -1, + Shard: -9223372036854775808, + SeqNo: 100500, + RootHash: bytes.Repeat([]byte{0x41}, 32), + FileHash: bytes.Repeat([]byte{0x42}, 32), + } + sessionID := bytes.Repeat([]byte{0x51}, 32) + const slot int32 = 77 + candidate := buildSimplexCandidate(t, block) + + toSign, err := buildSimplexToSignPayload(block, sessionID, slot, candidate) + if err != nil { + t.Fatalf("failed to build simplex payload: %v", err) + } + + sigs := []Signature{ + {NodeIDShort: id1, Signature: ed25519.Sign(pk1, toSign)}, + {NodeIDShort: id2, Signature: ed25519.Sign(pk2, toSign)}, + } + + if err = CheckBlockSignaturesSimplex(block, ccSeqno, setHash, sessionID, slot, candidate, sigs, validators); err != nil { + t.Fatalf("simplex signatures check failed: %v", err) + } +} + +func TestCheckBlockSignaturesSimplex_RejectsBlockIDPayload(t *testing.T) { + v1, pk1, id1 := makeSimplexValidator(t, 0x10, 40) + v2, pk2, id2 := makeSimplexValidator(t, 0x20, 35) + v3, _, _ := makeSimplexValidator(t, 0x30, 25) + validators := []*tlb.ValidatorAddr{v1, v2, v3} + + const ccSeqno uint32 = 8 + setHash, err := calcValidatorSetHash(ccSeqno, validators) + if err != nil { + t.Fatalf("failed to calc validator set hash: %v", err) + } + + block := &BlockIDExt{ + Workchain: -1, + Shard: -9223372036854775808, + SeqNo: 42, + RootHash: bytes.Repeat([]byte{0x31}, 32), + FileHash: bytes.Repeat([]byte{0x32}, 32), + } + sessionID := bytes.Repeat([]byte{0x61}, 32) + const slot int32 = 33 + candidate := buildSimplexCandidate(t, block) + + legacyPayload, err := tl.Serialize(BlockID{RootHash: block.RootHash, FileHash: block.FileHash}, true) + if err != nil { + t.Fatalf("failed to serialize block id payload: %v", err) + } + + sigs := []Signature{ + {NodeIDShort: id1, Signature: ed25519.Sign(pk1, legacyPayload)}, + {NodeIDShort: id2, Signature: ed25519.Sign(pk2, legacyPayload)}, + } + + err = CheckBlockSignaturesSimplex(block, ccSeqno, setHash, sessionID, slot, candidate, sigs, validators) + if err == nil { + t.Fatal("expected simplex check to fail for legacy block-id payload") + } + if !strings.Contains(err.Error(), "incorrect signature") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestBuildSimplexToSignPayload_CandidateBlockMismatch(t *testing.T) { + block := &BlockIDExt{ + Workchain: -1, + Shard: -9223372036854775808, + SeqNo: 100, + RootHash: bytes.Repeat([]byte{0x21}, 32), + FileHash: bytes.Repeat([]byte{0x22}, 32), + } + + wrongCandidateBlock := *block + wrongCandidateBlock.SeqNo++ + + candidate, err := tl.Serialize(ConsensusCandidateHashDataOrdinary{ + Block: wrongCandidateBlock, + CollatedFileHash: bytes.Repeat([]byte{0x77}, 32), + Parent: ConsensusCandidateWithoutParents{}, + }, true) + if err != nil { + t.Fatalf("failed to build candidate: %v", err) + } + + _, err = buildSimplexToSignPayload(block, bytes.Repeat([]byte{0x01}, 32), 1, candidate) + if err == nil { + t.Fatal("expected block id mismatch error") + } + if !strings.Contains(err.Error(), "block id mismatch") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/ton/wallet/integration_test.go b/ton/wallet/integration_test.go index a81d1b4a..c5337620 100644 --- a/ton/wallet/integration_test.go +++ b/ton/wallet/integration_test.go @@ -128,7 +128,7 @@ func Test_WalletTransfer(t *testing.T) { for _, v := range []VersionConfig{ConfigV5R1Final{ NetworkGlobalID: TestnetGlobalID, }, ConfigV5R1Beta{ - NetworkGlobalID: TestnetGlobalID, + NetworkGlobalID: TestnetGlobalID - 1, }, V3R2, V4R2, HighloadV2R2, V3R1, V4R1, HighloadV2Verified, ConfigHighloadV3{ MessageTTL: 120, MessageBuilder: func(ctx context.Context, subWalletId uint32) (id uint32, createdAt int64, err error) { @@ -185,7 +185,7 @@ func Test_WalletTransfer(t *testing.T) { tx, _, err := w.SendManyWaitTransaction(ctx, []*Message{tr}) if err != nil { - t.Fatal("Transfer err:", err.Error()) + t.Fatal("Transfer err:", w.WalletAddress().String(), err.Error()) return } @@ -194,7 +194,7 @@ func Test_WalletTransfer(t *testing.T) { return } } else { - t.Fatal("not enough balance") + t.Fatal(w.WalletAddress().Testnet(true).String(), "not enough balance") return } }) diff --git a/ton/wallet/wallet_test.go b/ton/wallet/wallet_test.go index 7cb9d4f4..76d96ea6 100644 --- a/ton/wallet/wallet_test.go +++ b/ton/wallet/wallet_test.go @@ -7,12 +7,13 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/xssnick/tonutils-go/liteclient" "math/big" "strings" "testing" "time" + "github.com/xssnick/tonutils-go/liteclient" + "github.com/xssnick/tonutils-go/ton" "github.com/xssnick/tonutils-go/address" @@ -628,6 +629,22 @@ func (w WaiterMock) GetTransaction(ctx context.Context, block *ton.BlockIDExt, a return w.MGetTransaction(ctx, block, addr, lt) } +func (w WaiterMock) GetOutMsgQueueSizes(ctx context.Context, wc *int32, shard *int64) (*ton.OutMsgQueueSizes, error) { + panic("implement me") +} + +func (w WaiterMock) GetBlockOutMsgQueueSize(ctx context.Context, block *ton.BlockIDExt) (*ton.BlockOutMsgQueueSize, error) { + panic("implement me") +} + +func (w WaiterMock) GetDispatchQueueInfo(ctx context.Context, block *ton.BlockIDExt, afterAddr *address.Address, maxAccounts int) (*ton.DispatchQueueInfo, error) { + panic("implement me") +} + +func (w WaiterMock) GetDispatchQueueMessages(ctx context.Context, block *ton.BlockIDExt, addr *address.Address, afterLT uint64, maxMessages int, options ...func(*ton.GetDispatchQueueMessages)) (*ton.DispatchQueueMessages, error) { + panic("implement me") +} + func TestDecryptCommentCell(t *testing.T) { if _seed == "" { t.Fatal(emptyWalletSeedEnvFatalMsg) diff --git a/tvm/cell/dict_test.go b/tvm/cell/dict_test.go index a2f38fe5..82d643fc 100644 --- a/tvm/cell/dict_test.go +++ b/tvm/cell/dict_test.go @@ -429,3 +429,24 @@ func TestDictionary_String(t *testing.T) { t.Fatal(d.String()) } } + +func TestDictionary_Sz(t *testing.T) { + d := NewDict(32) + + for i := 0; i < 14000; i++ { + n, _ := rand.Int(rand.Reader, big.NewInt(0xFFFFFFFF)) + d.SetIntKey(n, BeginCell().MustStoreRef(BeginCell().EndCell()).EndCell()) + } + + var calc func(cl *Cell) int + calc = func(cl *Cell) int { + var childs int + for i := 0; i < int(cl.RefsNum()); i++ { + childs += calc(cl.MustPeekRef(i)) + } + return childs + 1 + } + + c := d.AsCell() + println(c.Depth(), calc(c)) +} From 3a71247249a3d2a32acc9191fa9e8018abc60aa4 Mon Sep 17 00:00:00 2001 From: Oleg Baranov Date: Wed, 4 Mar 2026 17:13:23 +0400 Subject: [PATCH 5/6] TestAPIClient_GetLibraries test updated address to actual --- ton/integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ton/integration_test.go b/ton/integration_test.go index 83483396..dde107e2 100644 --- a/ton/integration_test.go +++ b/ton/integration_test.go @@ -743,7 +743,7 @@ func TestAPIClient_GetLibraries(t *testing.T) { defer cancel() ctx := apiTestNet.Client().StickyContext(_ctx) - addr := address.MustParseAddr("EQBi-jwMXO2AlSdhun2Th8lDr2jgsijuqWdyyD-ec-K1SYY1") + addr := address.MustParseAddr("0QDSbmZlj51noKgXhUmrfcIcjJXXtLgDis2ydvx8uKKqXhHQ") b, err := apiTestNet.CurrentMasterchainInfo(ctx) if err != nil { From ca7f8cd18722802219de78ac6ee7f7758d407ba9 Mon Sep 17 00:00:00 2001 From: Coverage Date: Wed, 4 Mar 2026 13:19:29 +0000 Subject: [PATCH 6/6] Updated coverage badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e792a4d..7738552b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Based on TON][ton-svg]][ton] [![Telegram Channel][tgc-svg]][tg-channel] -![Coverage](https://img.shields.io/badge/Coverage-70.2%25-brightgreen) +![Coverage](https://img.shields.io/badge/Coverage-70.0%25-brightgreen) Golang library for interacting with TON blockchain.