diff --git a/liteapi/account.go b/liteapi/account.go index acb123e0..c1d1ec46 100644 --- a/liteapi/account.go +++ b/liteapi/account.go @@ -1,59 +1,37 @@ package liteapi import ( - "context" + "bytes" "errors" "fmt" + "github.com/tonkeeper/tongo/boc" "github.com/tonkeeper/tongo/tlb" "github.com/tonkeeper/tongo/ton" ) -// GetAccountWithProof -// For safe operation, always use GetAccountWithProof with WithBlock(proofedBlock ton.BlockIDExt), as the proof of masterchain cashed blocks is not implemented yet! -func (c *Client) GetAccountWithProof(ctx context.Context, accountID ton.AccountID) (*tlb.ShardAccount, *tlb.ShardStateUnsplit, error) { - res, err := c.GetAccountStateRaw(ctx, accountID) // TODO: add proof check for masterHead - if err != nil { - return nil, nil, err - } - blockID := res.Id.ToBlockIdExt() - if len(res.Proof) == 0 { - return nil, nil, errors.New("empty proof") - } - +func checkAccountProof(accountID ton.AccountID, blockID ton.BlockIDExt, shardBlock ton.BlockIDExt, proof []*boc.Cell, shardProof []byte, stateProof *boc.Cell) (*tlb.ShardAccount, *tlb.ShardStateUnsplit, error) { var blockHash ton.Bits256 - if (accountID.Workchain == -1 && blockID.Workchain == -1) || blockID == res.Shardblk.ToBlockIdExt() { + if accountID.Workchain == -1 && blockID.Workchain == -1 || blockID == shardBlock { blockHash = blockID.RootHash } else { - if len(res.ShardProof) == 0 { + if len(shardProof) == 0 { return nil, nil, errors.New("empty shard proof") } - if res.Shardblk.RootHash == [32]byte{} { // TODO: how to check for empty shard? - return nil, nil, errors.New("shard block not passed") - } - shardHash := ton.Bits256(res.Shardblk.RootHash) - if _, err := checkShardInMasterProof(blockID, res.ShardProof, accountID.Workchain, shardHash); err != nil { + shardHash := shardBlock.RootHash + if _, err := checkShardInMasterProof(blockID, shardProof, accountID.Workchain, shardHash); err != nil { return nil, nil, fmt.Errorf("shard proof is incorrect: %w", err) } blockHash = shardHash } cellsMap := make(map[[32]byte]*boc.Cell) - if len(res.State) > 0 { - stateCells, err := boc.DeserializeBoc(res.State) - if err != nil { - return nil, nil, fmt.Errorf("state deserialization failed: %w", err) - } - hash, err := stateCells[0].Hash256() - if err != nil { - return nil, nil, fmt.Errorf("get hash err: %w", err) - } - cellsMap[hash] = stateCells[0] - } - proofCells, err := boc.DeserializeBoc(res.Proof) + hash, err := stateProof.Hash256WithLevel(0) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to get state hash: %w", err) } - shardState, err := checkBlockShardStateProof(proofCells, blockHash, cellsMap) + cellsMap[hash] = stateProof + + shardState, err := checkBlockShardStateProof(proof, blockHash, cellsMap) if err != nil { return nil, nil, fmt.Errorf("incorrect block proof: %w", err) } @@ -64,9 +42,7 @@ func (c *Client) GetAccountWithProof(ctx context.Context, accountID ton.AccountI return &values[i], shardState, nil } } - if len(res.State) == 0 { - return &tlb.ShardAccount{Account: tlb.Account{SumType: "AccountNone"}}, shardState, nil - } + return nil, nil, errors.New("invalid account state") } @@ -107,7 +83,7 @@ func checkBlockShardStateProof(proof []*boc.Cell, blockRootHash ton.Bits256, cel if len(proof) != 2 { return nil, errors.New("must be two root cells") } - block, err := checkBlockProof(*proof[0], blockRootHash) + block, err := checkProof[tlb.Block](*proof[0], blockRootHash, nil) if err != nil { return nil, fmt.Errorf("incorrect block proof: %w", err) } @@ -134,14 +110,148 @@ func checkBlockShardStateProof(proof []*boc.Cell, blockRootHash ton.Bits256, cel return &stateProof.Proof.VirtualRoot, nil } -func checkBlockProof(proof boc.Cell, blockRootHash ton.Bits256) (*tlb.MerkleProof[tlb.Block], error) { - var res tlb.MerkleProof[tlb.Block] - err := tlb.Unmarshal(&proof, &res) // merkle hash and depth checks inside +func checkProof[T any](proof boc.Cell, hash ton.Bits256, decoder *tlb.Decoder) (*tlb.MerkleProof[T], error) { + if decoder == nil { + decoder = tlb.NewDecoder() + } + var res tlb.MerkleProof[T] + err := decoder.Unmarshal(&proof, &res) // merkle hash and depth checks inside if err != nil { - return nil, fmt.Errorf("failed to unmarshal block proof: %w", err) + return nil, fmt.Errorf("failed to unmarshal proof: %w", err) } - if ton.Bits256(res.VirtualHash) != blockRootHash { - return nil, fmt.Errorf("invalid block root hash") + if ton.Bits256(res.VirtualHash) != hash { + return nil, fmt.Errorf("invalid hash") } return &res, nil // return new_hash field of MerkleUpdate of ShardState } + +func checkTxProof( + shardAccount tlb.HashmapAugE[tlb.Bits256, tlb.AccountBlock, tlb.CurrencyCollection], + accountAddr tlb.Bits256, + lt uint64, + hash ton.Bits256, +) error { + var accountBlock tlb.AccountBlock + for i, key := range shardAccount.Keys() { + if key.Equal(accountAddr) { + accountBlock = shardAccount.Values()[i] + break + } + } + + var accountTx *tlb.Transaction + for i, key := range accountBlock.Transactions.Keys() { + if uint64(key) == lt { + accountTx = &accountBlock.Transactions.Values()[i].Value + } + } + + if accountTx == nil { + return fmt.Errorf("tx not found") + } + + txHash := accountTx.Hash() + if !bytes.Equal(txHash[:], hash[:]) { + return fmt.Errorf("invalid tx hash") + } + + return nil +} + +type prunedBlock struct { + Magic tlb.Magic `tlb:"block#11ef55aa"` + GlobalId int32 + Info tlb.BlockInfo `tlb:"^"` + ValueFlow tlb.ValueFlow `tlb:"^"` + StateUpdate tlb.MerkleUpdate[tlb.ShardState] `tlb:"^"` + Extra prunedBlockExtra `tlb:"^"` +} + +type prunedBlockExtra struct { + Magic tlb.Magic `tlb:"block_extra#4a33f6fd"` + InMsgDescrCell boc.Cell `tlb:"^"` + OutMsgDescrCell boc.Cell `tlb:"^"` + AccountBlocks tlb.HashmapAugE[tlb.Bits256, prunedAccountBlock, tlb.CurrencyCollection] `tlb:"^"` + RandSeed tlb.Bits256 + CreatedBy tlb.Bits256 + Custom tlb.Maybe[tlb.Ref[tlb.McBlockExtra]] +} + +type prunedAccountBlock struct { + Magic tlb.Magic `tlb:"acc_trans#5"` + AccountAddr tlb.Bits256 + Transactions tlb.HashmapAug[tlb.Uint64, boc.Cell, tlb.CurrencyCollection] + StateUpdate tlb.HashUpdate `tlb:"^"` +} + +// this function doing same proof as checkTxProof but for pruned transactions that cannot be resolved +func checkPrunedTxProof( + shardAccount tlb.HashmapAugE[tlb.Bits256, prunedAccountBlock, tlb.CurrencyCollection], + accountAddr tlb.Bits256, + lt uint64, + hash ton.Bits256, +) error { + var accountBlock prunedAccountBlock + for i, key := range shardAccount.Keys() { + if key.Equal(accountAddr) { + accountBlock = shardAccount.Values()[i] + break + } + } + + var accountTx *boc.Cell + for i, key := range accountBlock.Transactions.Keys() { + if uint64(key) == lt { + accountTx = &accountBlock.Transactions.Values()[i] + } + } + + if accountTx == nil { + return fmt.Errorf("tx not found") + } + if len(accountTx.Refs()) == 0 { + return fmt.Errorf("shard account must have at least one ref in value") + } + + txHash, err := accountTx.Refs()[0].Hash256WithLevel(0) + if err != nil { + return err + } + if !bytes.Equal(txHash[:], hash[:]) { + return fmt.Errorf("invalid tx hash") + } + + return nil +} + +func getShardHashesHash(proofBlock boc.Cell, merkleProof *tlb.MerkleProof[tlb.Block]) (ton.Bits256, error) { + if !merkleProof.VirtualRoot.Extra.Custom.Exists { + return ton.Bits256{}, fmt.Errorf("mc block extra is missing in block") + } + + mcExtraCell := proofBlock.Refs()[0].Refs()[3].Refs()[3] + mcExtraCell.ResetCounters() + err := mcExtraCell.Skip(17) // 16 + 1 + if err != nil { + return ton.Bits256{}, err + } + hasShardHashesMap, err := mcExtraCell.ReadBit() + if err != nil { + return ton.Bits256{}, err + } + + mapCell := boc.NewCell() + err = mapCell.WriteBit(hasShardHashesMap) + if err != nil { + return ton.Bits256{}, err + } + + if hasShardHashesMap { + err = mapCell.AddRef(mcExtraCell.Refs()[0]) + if err != nil { + return ton.Bits256{}, err + } + } + + return mapCell.Hash256WithLevel(0) +} diff --git a/liteapi/account_test.go b/liteapi/account_test.go index 8b72c3f8..02662133 100644 --- a/liteapi/account_test.go +++ b/liteapi/account_test.go @@ -2,54 +2,16 @@ package liteapi import ( "bytes" - "context" "encoding/base64" "errors" "fmt" + "testing" + "github.com/tonkeeper/tongo/boc" "github.com/tonkeeper/tongo/tlb" "github.com/tonkeeper/tongo/ton" - "testing" ) -func TestGetAccountWithProof(t *testing.T) { - api, err := NewClient(Testnet(), FromEnvs()) - if err != nil { - t.Fatal(err) - } - testCases := []struct { - name string - accountID string - }{ - { - name: "account from masterchain", - accountID: "-1:34517c7bdf5187c55af4f8b61fdc321588c7ab768dee24b006df29106458d7cf", - }, - { - name: "active account from basechain", - accountID: "0:e33ed33a42eb2032059f97d90c706f8400bb256d32139ca707f1564ad699c7dd", - }, - { - name: "nonexisted from basechain", - accountID: "0:5f00decb7da51881764dc3959cec60609045f6ca1b89e646bde49d492705d77c", - }, - } - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - accountID, err := ton.AccountIDFromRaw(tt.accountID) - if err != nil { - t.Fatal("AccountIDFromRaw() failed: %w", err) - } - acc, st, err := api.GetAccountWithProof(context.TODO(), accountID) - if err != nil { - t.Fatal(err) - } - fmt.Printf("Account status: %v\n", acc.Account.Status()) - fmt.Printf("Last proof utime: %v\n", st.ShardStateUnsplit.GenUtime) - }) - } -} - func TestUnmarshallingProofWithPrunedResolver(t *testing.T) { testCases := []struct { name string @@ -124,48 +86,3 @@ func TestUnmarshallingProofWithPrunedResolver(t *testing.T) { }) } } - -func TestGetAccountWithProofForBlock(t *testing.T) { - api, err := NewClient(Testnet(), FromEnvs()) - if err != nil { - t.Fatal(err) - } - testCases := []struct { - name string - accountID string - block string - }{ - { - name: "active account from basechain", - accountID: "0:e33ed33a42eb2032059f97d90c706f8400bb256d32139ca707f1564ad699c7dd", - block: "(0,e000000000000000,24681072)", - }, - { - name: "account from masterchain", - accountID: "-1:34517c7bdf5187c55af4f8b61fdc321588c7ab768dee24b006df29106458d7cf", - block: "(-1,8000000000000000,23040403)", - }, - } - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - accountID, err := ton.AccountIDFromRaw(tt.accountID) - if err != nil { - t.Fatal("AccountIDFromRaw() failed: %w", err) - } - b, err := ton.ParseBlockID(tt.block) - if err != nil { - t.Fatal("ParseBlockID() failed: %w", err) - } - block, _, err := api.LookupBlock(context.TODO(), b, 1, nil, nil) - if err != nil { - t.Fatal("LookupBlock() failed: %w", err) - } - acc, st, err := api.WithBlock(block).GetAccountWithProof(context.TODO(), accountID) - if err != nil { - t.Fatal(err) - } - fmt.Printf("Account status: %v\n", acc.Account.Status()) - fmt.Printf("Last proof utime: %v\n", st.ShardStateUnsplit.GenUtime) - }) - } -} diff --git a/liteapi/client.go b/liteapi/client.go index 14a93015..50e9e32b 100644 --- a/liteapi/client.go +++ b/liteapi/client.go @@ -45,6 +45,7 @@ const ( // ProofPolicyUnsafe disables proof checks. ProofPolicyUnsafe ProofPolicy = iota ProofPolicyFast + ProofPolicySafe ) // Client provides a convenient way to interact with TON blockchain. @@ -71,6 +72,9 @@ type Client struct { // the underlying connections pool maintains information about which nodes are archive nodes. archiveDetectionEnabled bool + // trustedBlock is a block that will be used in proof chain verification as source block + trustedBlock *ton.BlockIDExt + // mu protects targetBlockID and networkGlobalID. mu sync.RWMutex targetBlockID *ton.BlockIDExt @@ -92,6 +96,8 @@ type Options struct { // should detect if its node is an archive node. DetectArchiveNodes bool + TrustedBlock *ton.BlockIDExt + SyncConnectionsInitialization bool PoolStrategy pool.Strategy } @@ -116,6 +122,13 @@ func WithMaxConnectionsNumber(maxConns int) Option { } } +func WithTrustedBlock(block ton.BlockIDExt) Option { + return func(o *Options) error { + o.TrustedBlock = &block + return nil + } +} + func WithWorkersPerConnection(workersNum int) Option { return func(o *Options) error { o.WorkersPerConnection = workersNum @@ -296,6 +309,7 @@ func NewClient(options ...Option) (*Client, error) { pool: connPool, proofPolicy: opts.ProofPolicy, archiveDetectionEnabled: opts.DetectArchiveNodes, + trustedBlock: opts.TrustedBlock, } go client.pool.Run(context.TODO()) return &client, nil @@ -322,7 +336,25 @@ func (c *Client) GetMasterchainInfo(ctx context.Context) (liteclient.LiteServerM if conn == nil { return liteclient.LiteServerMasterchainInfoC{}, pool.ErrNoConnections } - return conn.LiteServerGetMasterchainInfo(ctx) + mcInfo, err := conn.LiteServerGetMasterchainInfo(ctx) + if err != nil { + return liteclient.LiteServerMasterchainInfoC{}, err + } + if c.proofPolicy == ProofPolicySafe { + if c.trustedBlock == nil { + return liteclient.LiteServerMasterchainInfoC{}, fmt.Errorf("trusted block nil") + } + + if err = c.VerifyProofChain(ctx, *c.trustedBlock, mcInfo.Last.ToBlockIdExt()); err != nil { + return liteclient.LiteServerMasterchainInfoC{}, fmt.Errorf("failed to verify proof chain: %v", err) + } + + if mcInfo.Last.Seqno > c.trustedBlock.Seqno { + lastBlock := mcInfo.Last.ToBlockIdExt() + c.trustedBlock = &lastBlock + } + } + return mcInfo, nil } func (c *Client) GetMasterchainInfoExt(ctx context.Context, mode uint32) (liteclient.LiteServerMasterchainInfoExtC, error) { @@ -427,7 +459,7 @@ func (c *Client) GetBlockHeader(ctx context.Context, blockID ton.BlockIDExt, mod if err != nil { return tlb.BlockInfo{}, err } - _, info, err := decodeBlockHeader(res) + _, info, err := decodeBlockHeader(res, c.proofPolicy) return info, err } @@ -470,10 +502,10 @@ func (c *Client) LookupBlock(ctx context.Context, blockID ton.BlockID, mode uint if res.Id.ToBlockIdExt().BlockID != blockID { return ton.BlockIDExt{}, tlb.BlockInfo{}, BlockMismatch } - return decodeBlockHeader(res) + return decodeBlockHeader(res, c.proofPolicy) } -func decodeBlockHeader(header liteclient.LiteServerBlockHeaderC) (ton.BlockIDExt, tlb.BlockInfo, error) { +func decodeBlockHeader(header liteclient.LiteServerBlockHeaderC, policy ProofPolicy) (ton.BlockIDExt, tlb.BlockInfo, error) { cells, err := boc.DeserializeBoc(header.HeaderProof) if err != nil { return ton.BlockIDExt{}, tlb.BlockInfo{}, err @@ -481,6 +513,14 @@ func decodeBlockHeader(header liteclient.LiteServerBlockHeaderC) (ton.BlockIDExt if len(cells) != 1 { return ton.BlockIDExt{}, tlb.BlockInfo{}, boc.ErrNotSingleRoot } + if policy != ProofPolicyUnsafe { + headerProof, err := checkProof[tlb.BlockHeader](*cells[0], header.Id.ToBlockIdExt().RootHash, nil) + if err != nil { + return ton.BlockIDExt{}, tlb.BlockInfo{}, fmt.Errorf("failed to verify block header: %w", err) + } + + return header.Id.ToBlockIdExt(), headerProof.VirtualRoot.Info, nil + } var proof struct { Proof tlb.MerkleProof[tlb.BlockHeader] } @@ -519,8 +559,12 @@ func (c *Client) RunSmcMethodByID(ctx context.Context, accountID ton.AccountID, return 0, tlb.VmStack{}, err } blockID := c.targetBlockOr(masterHead) + mode := uint32(4) + if c.proofPolicy != ProofPolicyUnsafe { + mode += 3 // 0100 + 0011 + } req := liteclient.LiteServerRunSmcMethodRequest{ - Mode: 4, + Mode: mode, Id: liteclient.BlockIDExt(blockID), Account: liteclient.AccountID(accountID), MethodId: uint64(methodID), @@ -572,6 +616,19 @@ func (c *Client) GetAccountState(ctx context.Context, accountID ton.AccountID) ( if len(cells) != 1 { return tlb.ShardAccount{}, boc.ErrNotSingleRoot } + if c.proofPolicy != ProofPolicyUnsafe { + proof, err := boc.DeserializeBoc(res.Proof) + if err != nil { + return tlb.ShardAccount{}, fmt.Errorf("failed to deserialize proof: %w", err) + } + + acc, _, err := checkAccountProof(accountID, res.Id.ToBlockIdExt(), res.Shardblk.ToBlockIdExt(), proof, res.ShardProof, cells[0]) + if err != nil { + return tlb.ShardAccount{}, fmt.Errorf("failed to check account proof: %w", err) + } + + return *acc, nil + } var acc tlb.Account err = tlb.Unmarshal(cells[0], &acc) if err != nil { @@ -636,6 +693,12 @@ func (c *Client) GetShardInfo( if err != nil { return ton.BlockIDExt{}, err } + if c.proofPolicy != ProofPolicyUnsafe { + _, err = checkShardInMasterProof(blockID, res.ShardProof, int32(res.Shardblk.Workchain), ton.Bits256(res.Shardblk.RootHash)) + if err != nil { + return ton.BlockIDExt{}, fmt.Errorf("invalid proof: %v", err) + } + } return res.Id.ToBlockIdExt(), nil } @@ -676,6 +739,40 @@ func (c *Client) GetAllShardsInfo(ctx context.Context, blockID ton.BlockIDExt) ( if err != nil { return nil, err } + + if c.proofPolicy != ProofPolicyUnsafe { + proofCell, err := boc.DeserializeBoc(res.Proof) + if err != nil { + return nil, err + } + + if len(proofCell) == 1 { + merkleProof, err := checkProof[tlb.Block](*proofCell[0], blockID.RootHash, nil) + if err != nil { + return nil, fmt.Errorf("invalid block proof: %v", err) + } + + proofShardHashesHash, err := getShardHashesHash(*proofCell[0], merkleProof) + if err != nil { + return nil, fmt.Errorf("failed to get shard hash from proof: %v", err) + } + + shardHashesHash, err := cells[0].Hash() + if err != nil { + return nil, fmt.Errorf("failed to get shard hash: %v", err) + } + + if !bytes.Equal(shardHashesHash, proofShardHashesHash[:]) { + return nil, fmt.Errorf("invalid shard hashes") + } + } else { // 2 refs + _, err = checkShardInMasterProof(blockID, res.Proof, blockID.Workchain, blockID.RootHash) + if err != nil { + return nil, fmt.Errorf("invalid proof: %v", err) + } + } + } + var shards []ton.BlockIDExt for i, v := range inf.ShardHashes.Values() { wc := inf.ShardHashes.Keys()[i] @@ -735,6 +832,37 @@ func (c *Client) GetOneTransactionFromBlock( } var t tlb.Transaction err = tlb.Unmarshal(cells[0], &t) + if err != nil { + return ton.Transaction{}, err + } + txMap := map[[32]byte]*boc.Cell{ + t.Hash(): cells[0], + } + + if c.proofPolicy != ProofPolicyUnsafe { + txProof, err := boc.DeserializeBoc(r.Proof) + if err != nil { + return ton.Transaction{}, fmt.Errorf("failed to parse tx proof cell: %v", err) + } + + decoder := tlb.NewDecoder() + decoder = decoder.WithPrunedResolver(func(hash tlb.Bits256) (*boc.Cell, error) { + cl, ok := txMap[hash] + if ok { + return cl, nil + } + return nil, fmt.Errorf("not found") + }) + blockProof, err := checkProof[tlb.Block](*txProof[0], blockId.RootHash, decoder) + if err != nil { + return ton.Transaction{}, fmt.Errorf("failed to check block proof: %v", err) + } + + if err = checkTxProof(blockProof.VirtualRoot.Extra.AccountBlocks, t.AccountAddr, t.Lt, ton.Bits256(t.Hash())); err != nil { + return ton.Transaction{}, fmt.Errorf("failed to check tx proof: %v", err) + } + } + return ton.Transaction{Transaction: t, BlockID: r.Id.ToBlockIdExt()}, err } @@ -852,11 +980,41 @@ func (c *Client) ListBlockTransactions( mode, count uint32, after *liteclient.LiteServerTransactionId3C, ) ([]liteclient.LiteServerTransactionIdC, bool, error) { - // TODO: replace with tongo types + if c.proofPolicy != ProofPolicyUnsafe { + mode |= 1 << 5 + } res, err := c.ListBlockTransactionsRaw(ctx, blockID, mode, count, after) if err != nil { return nil, false, err } + var shardAccounts tlb.HashmapAugE[tlb.Bits256, prunedAccountBlock, tlb.CurrencyCollection] + if c.proofPolicy != ProofPolicyUnsafe { + txProof, err := boc.DeserializeBoc(res.Proof) + if err != nil { + return nil, false, fmt.Errorf("invalid tx proof: %v", err) + } + + blockProof, err := checkProof[prunedBlock](*txProof[0], res.Id.ToBlockIdExt().RootHash, nil) + if err != nil { + return nil, false, fmt.Errorf("invalid block proof: %v", err) + } + + shardAccounts = blockProof.VirtualRoot.Extra.AccountBlocks + } + + for _, id := range res.Ids { + // TODO: convert to tongo type + if id.Lt == nil || id.Hash == nil || id.Account == nil { + return nil, false, fmt.Errorf("invalid liteserver tx response") + } + + if c.proofPolicy != ProofPolicyUnsafe { + if err = checkPrunedTxProof(shardAccounts, tlb.Bits256(*id.Account), *id.Lt, ton.Bits256(*id.Hash)); err != nil { + return nil, false, fmt.Errorf("failed to check tx proof: %v", err) + } + } + } + return res.Ids, res.Incomplete, nil } @@ -977,10 +1135,42 @@ func (c *Client) GetConfigAllRaw(ctx context.Context, mode ConfigMode) (liteclie } func (c *Client) GetConfigParams(ctx context.Context, mode ConfigMode, paramList []uint32) (tlb.ConfigParams, error) { - client, masterHead, err := c.pool.BestMasterchainClient(ctx) + res, err := c.GetConfigParamsRaw(ctx, mode, paramList) + if err != nil { + return tlb.ConfigParams{}, err + } + if c.proofPolicy == ProofPolicyUnsafe { + return ton.DecodeConfigParams(res.ConfigProof) + } + stateProofCell, err := boc.DeserializeBoc(res.StateProof) + if err != nil { + return tlb.ConfigParams{}, err + } + if len(stateProofCell) != 1 { + return tlb.ConfigParams{}, fmt.Errorf("invalid number of roots in state proof boc") + } + configProofCell, err := boc.DeserializeBoc(res.ConfigProof) + if err != nil { + return tlb.ConfigParams{}, err + } + if len(configProofCell) != 1 { + return tlb.ConfigParams{}, fmt.Errorf("invalid number of roots in config proof boc") + } + shardState, err := checkBlockShardStateProof([]*boc.Cell{stateProofCell[0], configProofCell[0]}, ton.Bits256(res.Id.RootHash), nil) if err != nil { return tlb.ConfigParams{}, err } + if !shardState.ShardStateUnsplit.Custom.Exists { + return tlb.ConfigParams{}, fmt.Errorf("missing master chain state extra value") + } + return shardState.ShardStateUnsplit.Custom.Value.Value.Config, nil +} + +func (c *Client) GetConfigParamsRaw(ctx context.Context, mode ConfigMode, paramList []uint32) (liteclient.LiteServerConfigInfoC, error) { + client, masterHead, err := c.pool.BestMasterchainClient(ctx) + if err != nil { + return liteclient.LiteServerConfigInfoC{}, err + } blockID := c.targetBlockOr(masterHead) r, err := client.LiteServerGetConfigParams(ctx, liteclient.LiteServerGetConfigParamsRequest{ Mode: uint32(mode), @@ -988,12 +1178,12 @@ func (c *Client) GetConfigParams(ctx context.Context, mode ConfigMode, paramList ParamList: paramList, }) if err != nil { - return tlb.ConfigParams{}, err + return liteclient.LiteServerConfigInfoC{}, err } if r.Id.ToBlockIdExt() != blockID { - return tlb.ConfigParams{}, BlockMismatch + return liteclient.LiteServerConfigInfoC{}, BlockMismatch } - return ton.DecodeConfigParams(r.ConfigProof) + return r, nil } func (c *Client) GetValidatorStats( @@ -1025,23 +1215,38 @@ func (c *Client) GetValidatorStats( if r.Id.ToBlockIdExt() != blockID { return nil, BlockMismatch } - cells, err := boc.DeserializeBoc(r.DataProof) + dataProof, err := boc.DeserializeBoc(r.DataProof) if err != nil { return nil, err } - if len(cells) != 1 { + if len(dataProof) != 1 { return nil, boc.ErrNotSingleRoot } - var proof struct { - Proof tlb.MerkleProof[tlb.ShardState] // TODO: or ton.ShardStateUnsplit - } - err = tlb.Unmarshal(cells[0], &proof) - if err != nil { + var shardStateProof *tlb.MerkleProof[tlb.ShardStateUnsplit] + if err = tlb.Unmarshal(dataProof[0], &shardStateProof); err != nil { return nil, err } - // TODO: extract validator stats params from ShardState - // return &proof.Proof.VirtualRoot, nil //shards, nil - return nil, fmt.Errorf("not implemented") + if c.proofPolicy != ProofPolicyUnsafe { + stateProof, err := boc.DeserializeBoc(r.StateProof) + if err != nil { + return nil, fmt.Errorf("failed to deserialize state proof: %w", err) + } + + blockProof, err := checkProof[tlb.Block](*stateProof[0], masterHead.RootHash, nil) + if err != nil { + return nil, fmt.Errorf("failed to check block proof: %w", err) + } + + if ton.Bits256(shardStateProof.VirtualHash) != ton.Bits256(blockProof.VirtualRoot.StateUpdate.ToHash) { + return nil, fmt.Errorf("invalid shard state hash") + } + } + + if shardStateProof == nil || !shardStateProof.VirtualRoot.ShardStateUnsplit.Custom.Exists { + return nil, fmt.Errorf("missing validator stats in shard state") + } + + return &shardStateProof.VirtualRoot.ShardStateUnsplit.Custom.Value.Value, nil } func (c *Client) GetLibraries(ctx context.Context, libraryList []ton.Bits256) (map[ton.Bits256]*boc.Cell, error) { diff --git a/liteapi/client_test.go b/liteapi/client_test.go index 168ebbf8..43535b52 100644 --- a/liteapi/client_test.go +++ b/liteapi/client_test.go @@ -21,6 +21,28 @@ import ( "golang.org/x/exp/maps" ) +func getOldTrustedBlock() (*ton.BlockIDExt, error) { + var rootHash ton.Bits256 + err := rootHash.FromBase64("VpWyfNOLm8Rqt6CZZ9dZGqJRO3NyrlHHYN1k1oLbJ6g=") + if err != nil { + return nil, fmt.Errorf("incorrect root hash") + } + var fileHash ton.Bits256 + err = fileHash.FromBase64("8o12KX54BtJM8RERD1J97Qe1ZWk61LIIyXydlBnixK8=") + if err != nil { + return nil, fmt.Errorf("incorrect file hash") + } + return &ton.BlockIDExt{ + BlockID: ton.BlockID{ + Workchain: -1, + Shard: 9223372036854775808, + Seqno: 34835953, + }, + RootHash: rootHash, + FileHash: fileHash, + }, nil +} + func TestNewClient_WithMaxConnectionsNumber(t *testing.T) { t.Skip("when public lite servers are down, this test will fail") cli, err := NewClient(Mainnet()) @@ -653,3 +675,298 @@ func TestWaitMasterchainBlock(t *testing.T) { } fmt.Printf("Next block seqno : %v\n", bl.Seqno) } + +func TestGetMasterChainInfoWithSafePolicy(t *testing.T) { + unsafeAPI, err := NewClient(Mainnet(), FromEnvs()) + if err != nil { + t.Fatal(err) + } + trustedMcInfo, err := unsafeAPI.GetMasterchainInfo(context.Background()) + if err != nil { + t.Fatal(err) + } + trustedBlock := trustedMcInfo.Last.ToBlockIdExt() + + fmt.Printf("Trusted block seqno : %v\n", trustedBlock.Seqno) + + // wait for new block + time.Sleep(7 * time.Second) + + safeAPI, err := NewClient(Mainnet(), FromEnvs(), WithProofPolicy(ProofPolicySafe), WithTrustedBlock(trustedBlock)) + if err != nil { + t.Fatal(err) + } + latest, err := safeAPI.GetMasterchainInfo(context.Background()) + if err != nil { + t.Fatal(err) + } + fmt.Printf("Latest block seqno : %v\n", latest.Last.Seqno) +} + +func TestGetMasterchainInfoTrustedBlockUpdateWithSafePolicy(t *testing.T) { + trustedBlock, err := getOldTrustedBlock() + if err != nil { + t.Fatal(err) + } + fmt.Printf("Trusted block seqno : %v\n", trustedBlock.Seqno) + + safeAPI, err := NewClient(Mainnet(), FromEnvs(), WithProofPolicy(ProofPolicySafe), WithTrustedBlock(*trustedBlock)) + if err != nil { + t.Fatal(err) + } + latest, err := safeAPI.GetMasterchainInfo(context.Background()) + if err != nil { + t.Fatal(err) + } + fmt.Printf("Latest block seqno : %v\n", latest.Last.Seqno) + + time.Sleep(7 * time.Second) // wait for new block + + start := time.Now().Unix() + latest, err = safeAPI.GetMasterchainInfo(context.Background()) + if err != nil { + t.Fatal(err) + } + end := time.Now().Unix() + if end-start > 20 { + t.Fatal("too long for few blocks proof") + } + fmt.Printf("Latest block seqno : %v\n", latest.Last.Seqno) +} + +func TestRunSmcMethodWithFastPolicy(t *testing.T) { + api, err := NewClient(Mainnet(), FromEnvs(), WithProofPolicy(ProofPolicyFast)) + if err != nil { + t.Fatal(err) + } + + accountId := ton.MustParseAccountID("EQAs87W4yJHlF8mt29ocA4agnMrLsOP69jC1HPyBUjJay-7l") + _, _, err = api.RunSmcMethod(context.Background(), accountId, "seqno", tlb.VmStack{}) + if err != nil { + t.Fatal(err) + } +} + +func TestGetAccountStateWithFastPolicy(t *testing.T) { + api, err := NewClient(Mainnet(), FromEnvs(), WithProofPolicy(ProofPolicyFast)) + if err != nil { + t.Fatal(err) + } + + accountId := ton.MustParseAccountID("EQAs87W4yJHlF8mt29ocA4agnMrLsOP69jC1HPyBUjJay-7l") + state, err := api.GetAccountState(context.Background(), accountId) + if err != nil { + t.Fatal(err) + } + + fmt.Printf("Last tx lt: %v\n", state.LastTransLt) + fmt.Printf("Account status: %v\n", state.Account.Status()) +} + +func TestGetAllShardsInfoWithFastPolicy(t *testing.T) { + api, err := NewClient(Mainnet(), FromEnvs(), WithProofPolicy(ProofPolicyFast)) + if err != nil { + t.Fatal(err) + } + + mcInfo, err := api.GetMasterchainInfo(context.Background()) + if err != nil { + t.Fatal(err) + } + + info, err := api.GetAllShardsInfo(context.Background(), mcInfo.Last.ToBlockIdExt()) + if err != nil { + t.Fatal(err) + } + + for _, blk := range info { + fmt.Printf("Block's shard %v\n", blk.Shard) + fmt.Printf("Block's seqno %v\n", blk.Seqno) + } +} + +func TestGetOneTransactionFromBlockWithFastPolicy(t *testing.T) { + api, err := NewClient(Mainnet(), FromEnvs(), WithProofPolicy(ProofPolicyFast)) + if err != nil { + t.Fatal(err) + } + + accountId := ton.MustParseAccountID("EQAs87W4yJHlF8mt29ocA4agnMrLsOP69jC1HPyBUjJay-7l") + + var blockRootHash ton.Bits256 + blockRootHashByte, err := hex.DecodeString("6819eae0d10bb43fc6ff25a24cbbeeda67ab0fa798c42e12251c267955486cc1") + if err != nil { + t.Fatal(err) + } + copy(blockRootHash[:], blockRootHashByte) + + var blockFileHash ton.Bits256 + blockFileHashByte, err := hex.DecodeString("afc33a098806c62f4e8d84ecbb8405ace9cd3babbdb1dde5f09f4e01bd87af34") + if err != nil { + t.Fatal(err) + } + copy(blockFileHash[:], blockFileHashByte) + blockId := ton.BlockIDExt{ + BlockID: ton.BlockID{ + Workchain: 0, + Shard: 9223372036854775808, + Seqno: 57911816, + }, + RootHash: blockRootHash, + FileHash: blockFileHash, + } + + tx, err := api.GetOneTransactionFromBlock(context.Background(), accountId, blockId, 62522595000001) + if err != nil { + t.Fatal(err) + } + + fmt.Printf("Transaction lt %v\n", tx.Lt) +} + +func TestListBlockTransactionsWithFastPolicy(t *testing.T) { + api, err := NewClient(Mainnet(), FromEnvs(), WithProofPolicy(ProofPolicyFast)) + if err != nil { + t.Fatal(err) + } + + mcInfo, err := api.GetMasterchainInfo(context.Background()) + if err != nil { + t.Fatal(err) + } + + txs, isIncomplete, err := api.ListBlockTransactions(context.Background(), mcInfo.Last.ToBlockIdExt(), 7, 1000, nil) + if err != nil { + t.Fatal(err) + } + + fmt.Printf("Is block complete: %v\n", !isIncomplete) + for _, tx := range txs { + fmt.Printf("Tx hash: %v\n", hex.EncodeToString(tx.Hash[:])) + fmt.Printf("Tx lt: %v\n", *tx.Lt) + } +} + +func TestGetShardInfoWithFastPolicy(t *testing.T) { + api, err := NewClient(Mainnet(), FromEnvs(), WithProofPolicy(ProofPolicyFast)) + if err != nil { + t.Fatal(err) + } + + mcInfo, err := api.GetMasterchainInfo(context.Background()) + if err != nil { + t.Fatal(err) + } + + shrd, err := api.GetShardInfo(context.Background(), mcInfo.Last.ToBlockIdExt(), 0, 9223372036854775808, true) + if err != nil { + t.Fatal(err) + } + fmt.Printf("Shard: %v\n", shrd.Shard) + fmt.Printf("Shard seqno: %v\n", shrd.Seqno) +} + +func TestGetConfigParamsWithFastPolicy(t *testing.T) { + api, err := NewClient(Mainnet(), FromEnvs(), WithProofPolicy(ProofPolicyFast)) + if err != nil { + t.Fatal(err) + } + + params, err := api.GetConfigParams(context.Background(), NeedStateExtraRoot, []uint32{1}) + if err != nil { + t.Fatal(err) + } + + fmt.Printf("Config addr: %v\n", params.ConfigAddr.Hex()) +} + +func TestGetValidatorStatsWithFastPolicy(t *testing.T) { + api, err := NewClient(Mainnet(), FromEnvs(), WithProofPolicy(ProofPolicyFast)) + if err != nil { + t.Fatal(err) + } + + stats, err := api.GetValidatorStats(context.Background(), 0, 100, nil, nil) + if err != nil { + t.Fatal(err) + } + + fmt.Printf("Catchain seqno: %v\n", stats.Other.ValidatorInfo.CatchainSeqno) + fmt.Printf("Global balance in nano tons: %v\n", stats.GlobalBalance.Grams) +} + +func TestGetAccountWithFastPolicy(t *testing.T) { + api, err := NewClient(Testnet(), FromEnvs(), WithProofPolicy(ProofPolicyFast)) + if err != nil { + t.Fatal(err) + } + testCases := []struct { + name string + accountID string + }{ + { + name: "account from masterchain", + accountID: "-1:34517c7bdf5187c55af4f8b61fdc321588c7ab768dee24b006df29106458d7cf", + }, + { + name: "active account from basechain", + accountID: "0:e33ed33a42eb2032059f97d90c706f8400bb256d32139ca707f1564ad699c7dd", + }, + { + name: "nonexisted from basechain", + accountID: "0:5f00decb7da51881764dc3959cec60609045f6ca1b89e646bde49d492705d77c", + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + accountID, err := ton.AccountIDFromRaw(tt.accountID) + if err != nil { + t.Fatal("AccountIDFromRaw() failed: %w", err) + } + acc, err := api.GetAccountState(context.TODO(), accountID) + if err != nil { + t.Fatal(err) + } + fmt.Printf("Account status: %v\n", acc.Account.Status()) + }) + } +} + +func TestGetAccountWithProofForBlockWithFastPolicy(t *testing.T) { + api, err := NewClient(Testnet(), FromEnvs(), WithProofPolicy(ProofPolicyFast)) + if err != nil { + t.Fatal(err) + } + testCases := []struct { + name string + accountID string + block string + }{ + { + name: "account from masterchain", + accountID: "-1:34517c7bdf5187c55af4f8b61fdc321588c7ab768dee24b006df29106458d7cf", + block: "(-1,8000000000000000,23040403)", + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + accountID, err := ton.AccountIDFromRaw(tt.accountID) + if err != nil { + t.Fatal("AccountIDFromRaw() failed: %w", err) + } + b, err := ton.ParseBlockID(tt.block) + if err != nil { + t.Fatal("ParseBlockID() failed: %w", err) + } + block, _, err := api.LookupBlock(context.TODO(), b, 1, nil, nil) + if err != nil { + t.Fatalf("LookupBlock() failed: %v", err) + } + acc, err := api.WithBlock(block).GetAccountState(context.TODO(), accountID) + if err != nil { + t.Fatal(err) + } + fmt.Printf("Account status: %v\n", acc.Account.Status()) + }) + } +} diff --git a/liteapi/proof.go b/liteapi/proof.go index 0738bd8d..11eee4f7 100644 --- a/liteapi/proof.go +++ b/liteapi/proof.go @@ -8,13 +8,14 @@ import ( "encoding/binary" "encoding/hex" "fmt" + "hash/crc32" + "sort" + "github.com/tonkeeper/tongo/boc" "github.com/tonkeeper/tongo/liteclient" "github.com/tonkeeper/tongo/tl" "github.com/tonkeeper/tongo/tlb" "github.com/tonkeeper/tongo/ton" - "hash/crc32" - "sort" ) var castagnoliTable = crc32.MakeTable(crc32.Castagnoli) @@ -124,7 +125,7 @@ func verifyBackwardProofLink(toKeyBlock bool, source, target ton.BlockIDExt, des return fmt.Errorf("incorrect target block hash in proof") } // proof target block - targetBlock, err := checkBlockProof(*destProof, target.RootHash) + targetBlock, err := checkProof[tlb.Block](*destProof, target.RootHash, nil) if err != nil { return fmt.Errorf("failed to check target block proof: %w", err) } @@ -142,7 +143,7 @@ func verifyForwardProofLink(toKeyBlock bool, source, target ton.BlockIDExt, dest return fmt.Errorf("source seqno must be < target seqno for forward link") } // proof source block - sourceBlock, err := checkBlockProof(*configProof, source.RootHash) + sourceBlock, err := checkProof[tlb.Block](*configProof, source.RootHash, nil) if err != nil { return fmt.Errorf("failed to check source block proof: %w", err) } @@ -162,7 +163,7 @@ func verifyForwardProofLink(toKeyBlock bool, source, target ton.BlockIDExt, dest } catchainConfig := cfg.ConfigParam28.CatchainConfig // proof target block - targetBlock, err := checkBlockProof(*destProof, target.RootHash) + targetBlock, err := checkProof[tlb.Block](*destProof, target.RootHash, nil) if err != nil { return fmt.Errorf("failed to check target block proof: %w", err) } diff --git a/tlb/block.go b/tlb/block.go index 511fd76e..cbf3a48b 100644 --- a/tlb/block.go +++ b/tlb/block.go @@ -419,6 +419,7 @@ func (m *McBlockExtra) UnmarshalTLB(c *boc.Cell, decoder *Decoder) error { return fmt.Errorf("invalid tag") } + m.Magic = Magic(sumType) err = decoder.Unmarshal(c, &m.KeyBlock) if err != nil { return err diff --git a/tlb/hashmap.go b/tlb/hashmap.go index bda572cf..63782f7f 100644 --- a/tlb/hashmap.go +++ b/tlb/hashmap.go @@ -695,6 +695,11 @@ func (h HashmapAug[_, T1, _]) Values() []T1 { return h.values } +// Keys returns a list of keys of this hashmap. +func (h HashmapAug[keyT, _, _]) Keys() []keyT { + return h.keys +} + // Items returns key-value pairs of this hashmap. func (h HashmapE[keyT, T]) Items() []HashmapItem[keyT, T] { return h.m.Items() diff --git a/tlb/primitives.go b/tlb/primitives.go index efdf4c2d..38be48b6 100644 --- a/tlb/primitives.go +++ b/tlb/primitives.go @@ -228,7 +228,7 @@ func (m *Ref[T]) UnmarshalTLB(c *boc.Cell, decoder *Decoder) error { return err } if r.CellType() == boc.PrunedBranchCell { - cell := resolvePrunedCell(c, decoder.resolvePruned) + cell := resolvePrunedCell(r, decoder.resolvePruned) if cell == nil { var value T m.Value = value diff --git a/tlb/transactions.go b/tlb/transactions.go index 6e7bb187..27c2cf23 100644 --- a/tlb/transactions.go +++ b/tlb/transactions.go @@ -82,6 +82,7 @@ func (tx *Transaction) UnmarshalTLB(c *boc.Cell, decoder *Decoder) error { if sumType != 0b0111 { return fmt.Errorf("invalid tag") } + tx.Magic = Magic(sumType) if err = decoder.Unmarshal(c, &tx.AccountAddr); err != nil { return err }