From edd3f344333bf8ac2b8f29e1ea53bd8ed45198f7 Mon Sep 17 00:00:00 2001 From: Jakub Jerabek Date: Mon, 22 Jun 2026 17:18:54 +0200 Subject: [PATCH 1/7] fix(tron): the receipt for unsolidified block should not be set to nil so all token transfers are parsed and indexed correctly --- bchain/coins/tron/tronrpc.go | 8 +++-- bchain/coins/tron/txextra_test.go | 57 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/bchain/coins/tron/tronrpc.go b/bchain/coins/tron/tronrpc.go index 0fc653ffdf..9b9a4c8fd3 100644 --- a/bchain/coins/tron/tronrpc.go +++ b/bchain/coins/tron/tronrpc.go @@ -697,11 +697,11 @@ func (b *TronRPC) computeBlockConfirmations(blockNumber uint64) (uint32, error) return uint32(bestHeight - blockNumber + 1), nil } -func (b *TronRPC) buildTxFromHTTPData(txByID *tronGetTransactionByIDResponse, txInfo *tronGetTransactionInfoByIDResponse, blockTime int64, confirmations uint32, internalData *bchain.EthereumInternalData, isSolidified bool) (*bchain.Tx, error) { +func (b *TronRPC) buildTxFromHTTPData(txByID *tronGetTransactionByIDResponse, txInfo *tronGetTransactionInfoByIDResponse, blockTime int64, confirmations uint32, internalData *bchain.EthereumInternalData, keepReceipt bool) (*bchain.Tx, error) { csd := tronBuildEthereumSpecificData(txByID, txInfo) csd.InternalData = internalData - if !isSolidified { + if !keepReceipt { csd.Receipt = nil // set to nil so it can be considered as pending } @@ -948,7 +948,9 @@ func (b *TronRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { txInternalData = &internalData[i] } - rebuiltTx, err := b.buildTxFromHTTPData(txByID, txInfo, bbh.Time, confirmations, txInternalData, isSolidified) + // Keep receipts for block indexing even before solidity. If logs are dropped + // here, token-transfer participants never reach the address/contract indexes. + rebuiltTx, err := b.buildTxFromHTTPData(txByID, txInfo, bbh.Time, confirmations, txInternalData, true) if err != nil { return nil, err } diff --git a/bchain/coins/tron/txextra_test.go b/bchain/coins/tron/txextra_test.go index f7ed4de45a..64eca6721f 100644 --- a/bchain/coins/tron/txextra_test.go +++ b/bchain/coins/tron/txextra_test.go @@ -512,3 +512,60 @@ func TestTronBuildTxFromHTTPData_WithSynthesizedGenesisData(t *testing.T) { require.NotNil(t, receipt) require.Equal(t, "0x1", receipt.Status) } + +func TestTronBuildTxFromHTTPData_KeepReceiptControlsTokenLogs(t *testing.T) { + txByID := &tronGetTransactionByIDResponse{ + TxID: "b7a97862b0c719b714f0cf7e250ebc2dcdf1e5f05c54ddb21ff3a748c1aa45e4", + } + txByID.RawData.Contract = []tronTxContract{{ + Type: "TriggerSmartContract", + }} + txByID.RawData.Contract[0].Parameter.Value.OwnerAddress = "410746a05c314538e3e21faae3d702cc7939efc07a" + txByID.RawData.Contract[0].Parameter.Value.ContractAddress = "4139dd12a54e2bab7c82aa14a1e158b34263d2d510" + txByID.RawData.Contract[0].Parameter.Value.Data = "6f21b898" + + txInfo := &tronGetTransactionInfoByIDResponse{ + ID: txByID.TxID, + Log: []*bchain.RpcLog{ + { + Address: "a614f803b6fd780986a42c78ec9c7f77e6ded13c", + Data: "000000000000000000000000000000000000000000000000000000002d4cae00", + Topics: []string{ + "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0000000000000000000000000746a05c314538e3e21faae3d702cc7939efc07a", + "000000000000000000000000f14cca91e8d5fff29cbcd42b26a19a7395b1aa84", + }, + }, + }, + } + txInfo.Receipt.Result = "SUCCESS" + + parser := NewTronParser(1, false) + tronRPC := &TronRPC{ + Parser: parser, + } + + pendingTx, err := tronRPC.buildTxFromHTTPData(txByID, txInfo, 0, 0, nil, false) + require.NoError(t, err) + pendingSpecific, ok := pendingTx.CoinSpecificData.(bchain.EthereumSpecificData) + require.True(t, ok) + require.Nil(t, pendingSpecific.Receipt) + pendingTransfers, err := parser.EthereumTypeGetTokenTransfersFromTx(pendingTx) + require.NoError(t, err) + require.Empty(t, pendingTransfers) + + indexedTx, err := tronRPC.buildTxFromHTTPData(txByID, txInfo, 0, 1, nil, true) + require.NoError(t, err) + indexedSpecific, ok := indexedTx.CoinSpecificData.(bchain.EthereumSpecificData) + require.True(t, ok) + require.NotNil(t, indexedSpecific.Receipt) + require.Len(t, indexedSpecific.Receipt.Logs, 1) + + indexedTransfers, err := parser.EthereumTypeGetTokenTransfersFromTx(indexedTx) + require.NoError(t, err) + require.Len(t, indexedTransfers, 1) + require.Equal(t, "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", indexedTransfers[0].Contract) + require.Equal(t, "TAdgLcNFZjeF1AspMobkz2bbxs5yDXpwYx", indexedTransfers[0].From) + require.Equal(t, "TXy5qqpAJNykdqsMU2dZM7B7mZbVF2izAN", indexedTransfers[0].To) + require.Equal(t, "760000000", indexedTransfers[0].Value.String()) +} From 72a11912d9ba65c78e9eaa22219eeaa7d5c9819b Mon Sep 17 00:00:00 2001 From: Jakub Jerabek Date: Wed, 24 Jun 2026 20:13:18 +0200 Subject: [PATCH 2/7] feat(tron): increase number of connections in the HTTP client to speed up syncing --- bchain/coins/tron/tronhttp.go | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/bchain/coins/tron/tronhttp.go b/bchain/coins/tron/tronhttp.go index 5172e37ce5..cd138db1c6 100644 --- a/bchain/coins/tron/tronhttp.go +++ b/bchain/coins/tron/tronhttp.go @@ -9,6 +9,27 @@ import ( "time" ) +// Connection-pool sizing for the Tron HTTP node clients. +// +// Each TronHTTPClient talks to a single node (full node or solidity node), so +// these caps are effectively per-node. During initial sync GetBlock issues two +// concurrent solidity calls per block (getblockbynum + gettransactioninfobyblocknum) +// across a pool of block-sync workers, so the peak in-flight request count to one +// node is well above Go's http.DefaultTransport default of 2 idle connections per +// host. With only 2 idle connections kept, the transport re-dials (TCP + TLS) on +// every demand dip; those fresh dials land on the request critical path and form +// the latency tail. Benchmarking the sync call pattern (contrib/scripts/tron-sync-bench) +// against a real node showed raising this cap removes the accumulated handshake tax +// and cuts sync-path p99 by ~32% with ~14% higher block throughput. +// +// tronMaxIdleConnsPerHost is sized to comfortably exceed the sync fan-out to one +// node; tronMaxIdleConns is the per-transport global cap, kept above the per-host +// cap so it does not bind first. +const ( + tronMaxIdleConnsPerHost = 64 + tronMaxIdleConns = 100 +) + type TronHTTP interface { Request(ctx context.Context, path string, reqBody interface{}, respBody interface{}) error } @@ -19,10 +40,15 @@ type TronHTTPClient struct { } func NewTronHTTPClient(baseURL string, timeout time.Duration) *TronHTTPClient { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.MaxIdleConns = tronMaxIdleConns + transport.MaxIdleConnsPerHost = tronMaxIdleConnsPerHost + return &TronHTTPClient{ baseURL: baseURL, httpClient: &http.Client{ - Timeout: timeout, + Timeout: timeout, + Transport: transport, }, } } From cae749d0779746395e1b6827f8b637bbbcfe6fc9 Mon Sep 17 00:00:00 2001 From: Jakub Jerabek Date: Wed, 24 Jun 2026 20:15:55 +0200 Subject: [PATCH 3/7] feat(tron): remove zeromq nil check to reduce number of requests per block to speed up syncing --- bchain/coins/tron/tronhttp.go | 16 ---------------- bchain/coins/tron/tronrpc.go | 31 ------------------------------- 2 files changed, 47 deletions(-) diff --git a/bchain/coins/tron/tronhttp.go b/bchain/coins/tron/tronhttp.go index cd138db1c6..2c21f1c7e8 100644 --- a/bchain/coins/tron/tronhttp.go +++ b/bchain/coins/tron/tronhttp.go @@ -9,22 +9,6 @@ import ( "time" ) -// Connection-pool sizing for the Tron HTTP node clients. -// -// Each TronHTTPClient talks to a single node (full node or solidity node), so -// these caps are effectively per-node. During initial sync GetBlock issues two -// concurrent solidity calls per block (getblockbynum + gettransactioninfobyblocknum) -// across a pool of block-sync workers, so the peak in-flight request count to one -// node is well above Go's http.DefaultTransport default of 2 idle connections per -// host. With only 2 idle connections kept, the transport re-dials (TCP + TLS) on -// every demand dip; those fresh dials land on the request critical path and form -// the latency tail. Benchmarking the sync call pattern (contrib/scripts/tron-sync-bench) -// against a real node showed raising this cap removes the accumulated handshake tax -// and cuts sync-path p99 by ~32% with ~14% higher block throughput. -// -// tronMaxIdleConnsPerHost is sized to comfortably exceed the sync fan-out to one -// node; tronMaxIdleConns is the per-transport global cap, kept above the per-host -// cap so it does not bind first. const ( tronMaxIdleConnsPerHost = 64 tronMaxIdleConns = 100 diff --git a/bchain/coins/tron/tronrpc.go b/bchain/coins/tron/tronrpc.go index 9b9a4c8fd3..6cfcd1a4d1 100644 --- a/bchain/coins/tron/tronrpc.go +++ b/bchain/coins/tron/tronrpc.go @@ -347,21 +347,6 @@ func (b *TronRPC) GetBlockInfo(hash string) (*bchain.BlockInfo, error) { } func (b *TronRPC) getBestHeader() (bchain.EVMHeader, error) { - // During initial sync (before ZeroMQ is initialized) there is no push-based - // tip refresh, so always read the latest header from the backend. - if b.mq == nil { - _, err := b.refreshBestHeaderFromChain() - if err != nil { - return nil, err - } - b.bestHeaderLock.Lock() - defer b.bestHeaderLock.Unlock() - if b.bestHeader == nil || b.bestHeader.Number() == nil { - return nil, errors.New("best header is nil") - } - return b.bestHeader, nil - } - b.bestHeaderLock.Lock() cachedHeader := b.bestHeader cachedAt := b.bestHeaderTime @@ -384,22 +369,6 @@ func (b *TronRPC) getBestHeader() (bchain.EVMHeader, error) { return b.bestHeader, nil } -// setBestHeader stores h as the cached tip and reports whether it changed. -// -// Unlike EthereumRPC.setBestHeader this is intentionally NON-monotonic: a lower -// height is accepted. Tron's tip is never taken from the feed header (the ZeroMQ -// notification carries none) — it is always an HTTP re-query (refreshBestHeaderFromChain), -// refreshed on every notification and on a tronBestHeaderMaxAge timer, so the cache -// is meant to track whatever the backend currently reports. Accepting a lower height -// is what lets a genuine rollback surface immediately to resyncIndex, so Tron is not -// subject to the frozen-tip masking that the EVM monotonic guard introduces (and which -// EVM has to undo with a watchdog regress). -// -// Tradeoff: with a load-balanced Tron RPC, a single lagging node answering a re-query -// could regress the tip and trip a spurious fork in resyncIndex (the case the EVM -// monotonic guard exists to prevent). That is acceptable for the common single-node -// java-tron backend; if Tron is ever fronted by a load balancer, port the EVM pattern -// here (monotonic hot path + on-advance liveness + allowRegress watchdog poll). func (b *TronRPC) setBestHeader(h bchain.EVMHeader) bool { if h == nil || h.Number() == nil { return false From dd1b64d292d90b03635b9c6efc40b233ea888272 Mon Sep 17 00:00:00 2001 From: Jakub Jerabek Date: Wed, 24 Jun 2026 21:28:11 +0200 Subject: [PATCH 4/7] feat(tron): use HTTP endpoint for getting block has by number to fasten sequential sync path --- bchain/coins/tron/tronhttp_endpoints.go | 24 ++++++++++++++++++++++++ bchain/coins/tron/tronrpc.go | 10 ++++++++++ 2 files changed, 34 insertions(+) diff --git a/bchain/coins/tron/tronhttp_endpoints.go b/bchain/coins/tron/tronhttp_endpoints.go index 0e117033fe..f331117e05 100644 --- a/bchain/coins/tron/tronhttp_endpoints.go +++ b/bchain/coins/tron/tronhttp_endpoints.go @@ -68,6 +68,7 @@ type tronGetBlockResponse struct { } type tronGetBlockHeaderResponse struct { + BlockID string `json:"blockID"` BlockHeader struct { RawData struct { Number *uint64 `json:"number"` @@ -467,6 +468,29 @@ func (b *TronRPC) requestBlockByID(ctx context.Context, blockHash string, isSoli return &resp, nil } +func (b *TronRPC) requestBlockHashByNum(ctx context.Context, blockNum uint32, isSolidified bool) (string, error) { + req := map[string]any{ + "id_or_num": strconv.FormatUint(uint64(blockNum), 10), + "detail": false, + } + http := b.getLookupHTTPClient(isSolidified) + var resp tronGetBlockHeaderResponse + if err := http.Request(ctx, tronLookupPath(isSolidified, "/wallet/getblock", "/walletsolidity/getblock"), req, &resp); err != nil { + return "", err + } + if resp.BlockHeader.RawData.Number == nil { + return "", errors.Errorf("Tron getblock returned missing block_header.raw_data.number for height %d", blockNum) + } + if *resp.BlockHeader.RawData.Number != uint64(blockNum) { + return "", errors.Errorf("Tron getblock returned height %d, want %d", *resp.BlockHeader.RawData.Number, blockNum) + } + hash := strip0xPrefix(resp.BlockID) + if hash == "" { + return "", errors.Errorf("Tron getblock returned empty blockID for height %d", blockNum) + } + return hash, nil +} + func (b *TronRPC) requestLatestSolidifiedBlockHeight(ctx context.Context) (uint64, error) { http := b.solidityNodeHTTP if http == nil { diff --git a/bchain/coins/tron/tronrpc.go b/bchain/coins/tron/tronrpc.go index 6cfcd1a4d1..b2bb171c15 100644 --- a/bchain/coins/tron/tronrpc.go +++ b/bchain/coins/tron/tronrpc.go @@ -287,6 +287,16 @@ func (b *TronRPC) GetBestBlockHash() (string, error) { // GetBlockHash returns block hash in Tron API format (without 0x prefix). func (b *TronRPC) GetBlockHash(height uint32) (string, error) { + if b.isBlockSolidified(uint64(height)) && b.solidityNodeHTTP != nil { + ctx, cancel := context.WithTimeout(b.requestContext(), b.Timeout) + defer cancel() + hash, err := b.requestBlockHashByNum(ctx, height, true) + if err == nil { + return hash, nil + } + glog.V(1).Infof("Tron native getblock hash lookup failed for height %d, falling back to JSON-RPC: %v", height, err) + } + hash, err := b.EthereumRPC.GetBlockHash(height) if err != nil { return "", err From a5da37d564e90490f838806adda61a416cfa0528 Mon Sep 17 00:00:00 2001 From: pragmaxim Date: Thu, 25 Jun 2026 06:53:50 +0200 Subject: [PATCH 5/7] fix(tron): drain HTTP response body before close to enable connection reuse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit net/http only returns a connection to the idle pool when the response body is read to EOF and then closed. Request() closed the body without draining: the >=300 early return reads nothing, and json.Decoder can leave trailing bytes unconsumed on larger responses, so connections were torn down instead of pooled — defeating the MaxIdleConns/MaxIdleConnsPerHost tuning this branch added to speed up syncing. Drain the body in the deferred close. Co-Authored-By: Claude Opus 4.8 --- bchain/coins/tron/tronhttp.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bchain/coins/tron/tronhttp.go b/bchain/coins/tron/tronhttp.go index 2c21f1c7e8..91d7258f82 100644 --- a/bchain/coins/tron/tronhttp.go +++ b/bchain/coins/tron/tronhttp.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "time" ) @@ -53,7 +54,14 @@ func (c *TronHTTPClient) Request(ctx context.Context, path string, reqBody inter if err != nil { return fmt.Errorf("HTTP error calling Tron API %s: %w", path, err) } - defer resp.Body.Close() + // Drain the body before closing so net/http can return the connection to the + // idle pool for keep-alive reuse. Without this, the >=300 early return (which + // reads nothing) and any bytes the JSON decoder leaves unconsumed would force + // the connection closed, defeating the MaxIdleConns* pooling configured above. + defer func() { + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + }() if resp.StatusCode >= 300 { return fmt.Errorf("Tron API returned status %d at path: %s %s", resp.StatusCode, c.baseURL, path) From f2115fc8c1f425bd0961512adbe0f76a7285957b Mon Sep 17 00:00:00 2001 From: pragmaxim Date: Thu, 25 Jun 2026 06:53:50 +0200 Subject: [PATCH 6/7] refactor(tron): drop dead isSolidified param from requestBlockHashByNum requestBlockHashByNum is only called from GetBlockHash's fast path, which is guarded by isBlockSolidified() && solidityNodeHTTP \!= nil, so the sole caller always passed true and the /wallet/getblock (full-node) branch was unreachable. Hardcode the solidity-node client and /walletsolidity/getblock path so the function's actual single-purpose role is explicit and no untested branch lingers. Co-Authored-By: Claude Opus 4.8 --- bchain/coins/tron/tronhttp_endpoints.go | 9 ++++++--- bchain/coins/tron/tronrpc.go | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/bchain/coins/tron/tronhttp_endpoints.go b/bchain/coins/tron/tronhttp_endpoints.go index f331117e05..983939225d 100644 --- a/bchain/coins/tron/tronhttp_endpoints.go +++ b/bchain/coins/tron/tronhttp_endpoints.go @@ -468,14 +468,17 @@ func (b *TronRPC) requestBlockByID(ctx context.Context, blockHash string, isSoli return &resp, nil } -func (b *TronRPC) requestBlockHashByNum(ctx context.Context, blockNum uint32, isSolidified bool) (string, error) { +// requestBlockHashByNum resolves a block hash by height via the solidity node's +// /walletsolidity/getblock endpoint. It is only called for already-solidified +// heights (see GetBlockHash), so the lookup always targets the solidity node. +func (b *TronRPC) requestBlockHashByNum(ctx context.Context, blockNum uint32) (string, error) { req := map[string]any{ "id_or_num": strconv.FormatUint(uint64(blockNum), 10), "detail": false, } - http := b.getLookupHTTPClient(isSolidified) + http := b.getLookupHTTPClient(true) var resp tronGetBlockHeaderResponse - if err := http.Request(ctx, tronLookupPath(isSolidified, "/wallet/getblock", "/walletsolidity/getblock"), req, &resp); err != nil { + if err := http.Request(ctx, "/walletsolidity/getblock", req, &resp); err != nil { return "", err } if resp.BlockHeader.RawData.Number == nil { diff --git a/bchain/coins/tron/tronrpc.go b/bchain/coins/tron/tronrpc.go index b2bb171c15..cd9299a208 100644 --- a/bchain/coins/tron/tronrpc.go +++ b/bchain/coins/tron/tronrpc.go @@ -290,7 +290,7 @@ func (b *TronRPC) GetBlockHash(height uint32) (string, error) { if b.isBlockSolidified(uint64(height)) && b.solidityNodeHTTP != nil { ctx, cancel := context.WithTimeout(b.requestContext(), b.Timeout) defer cancel() - hash, err := b.requestBlockHashByNum(ctx, height, true) + hash, err := b.requestBlockHashByNum(ctx, height) if err == nil { return hash, nil } From a886dc644ca9822de3d5322baed79e0e13c0cf8d Mon Sep 17 00:00:00 2001 From: Jakub Jerabek Date: Thu, 25 Jun 2026 09:11:40 +0200 Subject: [PATCH 7/7] docs(tron): add initial sync tuning --- docs/tron-config.md | 5 +++ docs/tron-sync-tuning.md | 78 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 docs/tron-sync-tuning.md diff --git a/docs/tron-config.md b/docs/tron-config.md index 8cfd24546c..107abf2634 100644 --- a/docs/tron-config.md +++ b/docs/tron-config.md @@ -61,7 +61,12 @@ Set explicit `tron_fullnode_http_url_template` and `tron_solidity_http_url_templ * the scheme differs from the JSON-RPC endpoint * the deployment uses non-standard ports +## Sync Performance Tuning + +For initial-sync tuning notes, see [Tron Sync Tuning](/docs/tron-sync-tuning.md). + ## Related Docs * [Config](/docs/config.md) +* [Tron Sync Tuning](/docs/tron-sync-tuning.md) * [Testing](/docs/testing.md) diff --git a/docs/tron-sync-tuning.md b/docs/tron-sync-tuning.md new file mode 100644 index 0000000000..33d3104f49 --- /dev/null +++ b/docs/tron-sync-tuning.md @@ -0,0 +1,78 @@ +# Tron Sync Tuning + +Fast Tron initial sync needs tuning on both sides of the RPC boundary. In the +observed setup, the bottleneck was backend block-hash/block lookup latency, not +Blockbook Go GC or RocksDB writes. + +## java-tron + +Keep the backend on LevelDB, keep transaction history enabled, enable the LevelDB read tuning and raise the +HTTP connection limit enough for Blockbook's worker count: + +```hocon +storage { + db.engine = "LEVELDB" + db.sync = false + transHistory.switch = "on" + + # LevelDB read tuning from Tron TIP-343. Raises fd usage. + default = { + maxOpenFiles = 100 + } + defaultM = { + maxOpenFiles = 500 + } + defaultL = { + maxOpenFiles = 1000 + } +} + +node { + # With Blockbook -workers=32, start around 200. + maxHttpConnectNumber = 200 +} +``` + +Run java-tron with enough heap, but leave RAM for the OS page cache. On large +hosts, `-Xms32g -Xmx32g` with G1GC is a good starting point. Validate with GC +logs and disk latency; a larger heap is not automatically faster. + +Keep event subscription minimal for Blockbook sync: + +```hocon +event.subscribe { + topics = [ + { triggerName = "block", enable = true, topic = "block", solidified = false }, + { triggerName = "solidity", enable = true, topic = "solidity" } + ] +} +``` + +Do not enable `transaction`, `contractevent`, or `contractlog` topics just for +Blockbook initial sync. + +## Blockbook + +Run initial sync with enough workers to keep java-tron busy: + +```ini +ExecStart=.../blockbook ... -sync -workers=32 ... +``` + +Pair this with a sufficiently large Tron HTTP connection pool in Blockbook and +`node.maxHttpConnectNumber` in java-tron. Raising workers without raising the +backend connection limit just moves the queue. + +## Verify + +Useful indicators while syncing: + +* Blockbook log `elapsed` per 1000 blocks +* `blockbook_rpc_latency{method="GetBlockHash"}` and `method="GetBlock"`, increased latency means the bottleneck is on the Java-Tron side +* java-tron GC pauses and heap after GC +* disk utilization and await, for example `iostat -xz 1` +* open file descriptors for both services +* `blockbook_index_block_not_found_retries` + +With the tuning above and native hash lookup, observed throughput reached about +7.5-9.5 seconds per 1000 historical blocks on the tested setup.