Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pruned resolver #299

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions boc/cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,5 +436,37 @@ func (c *Cell) GetMerkleRoot() ([32]byte, error) {
var hash [32]byte
copy(hash[:], bytes[1:])
return hash, nil
}

// TODO: move to deserializer
func (c *Cell) isValidMerkleProofCell() bool {
return c.cellType == MerkleProofCell && c.RefsSize() == 1 && c.BitSize() == 280
}

func (c *Cell) CalculateMerkleProofMeta() (int, [32]byte, error) {
if !c.isValidMerkleProofCell() {
return 0, [32]byte{}, errors.New("not valid merkle proof cell")
}
imc, err := newImmutableCell(c.Refs()[0], map[*Cell]*immutableCell{})
if err != nil {
return 0, [32]byte{}, fmt.Errorf("get immutable cell: %w", err)
}
h := imc.Hash(0)
var hash [32]byte
copy(hash[:], h)
depth := imc.Depth(0)
return depth, hash, nil
}

// TODO: or add level as optional parameter to Hash256()
func (c *Cell) Hash256WithLevel(level int) ([32]byte, error) {
// TODO: or check for pruned cell and read hash directly from cell
imc, err := newImmutableCell(c, map[*Cell]*immutableCell{})
if err != nil {
return [32]byte{}, err
}
b := imc.Hash(level)
var h [32]byte
copy(h[:], b)
return h, nil
}
143 changes: 143 additions & 0 deletions liteapi/account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package liteapi

import (
"context"
"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")
}

var blockHash ton.Bits256
if (accountID.Workchain == -1 && blockID.Workchain == -1) || blockID == res.Shardblk.ToBlockIdExt() {
blockHash = blockID.RootHash
} else {
if len(res.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 {
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]
}
shardState, err := checkBlockShardStateProof(res.Proof, blockHash, cellsMap)
if err != nil {
return nil, nil, fmt.Errorf("incorrect block proof: %w", err)
}
values := shardState.ShardStateUnsplit.Accounts.Values()
keys := shardState.ShardStateUnsplit.Accounts.Keys()
for i, k := range keys {
if k == accountID.Address {
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")
}

func checkShardInMasterProof(master ton.BlockIDExt, shardProof []byte, workchain int32, shardRootHash ton.Bits256) error {
shardState, err := checkBlockShardStateProof(shardProof, master.RootHash, nil)
if err != nil {
return fmt.Errorf("check block proof failed: %w", err)
}
if !shardState.ShardStateUnsplit.Custom.Exists {
return fmt.Errorf("not a masterchain block")
}
stateExtra := shardState.ShardStateUnsplit.Custom.Value.Value
keys := stateExtra.ShardHashes.Keys()
values := stateExtra.ShardHashes.Values()
for i, k := range keys {
binTreeValues := values[i].Value.BinTree.Values
for _, b := range binTreeValues {
switch b.SumType {
case "Old":
if int32(k) == workchain && ton.Bits256(b.Old.RootHash) == shardRootHash {
return nil
}
case "New":
if int32(k) == workchain && ton.Bits256(b.New.RootHash) == shardRootHash {
return nil
}
}
}
}
return fmt.Errorf("required shard hash not found in proof")
}

func checkBlockShardStateProof(proof []byte, blockRootHash ton.Bits256, cellsMap map[[32]byte]*boc.Cell) (*tlb.ShardStateUnsplit, error) {
proofCells, err := boc.DeserializeBoc(proof)
if err != nil {
return nil, err
}
if len(proofCells) != 2 {
return nil, errors.New("must be two root cells")
}
newStateHash, err := checkBlockProof(proofCells[0], blockRootHash)
if err != nil {
return nil, fmt.Errorf("incorrect block proof: %w", err)
}
var stateProof struct {
Proof tlb.MerkleProof[tlb.ShardStateUnsplit]
}
decoder := tlb.NewDecoder()
if cellsMap != nil {
decoder = decoder.WithPrunedResolver(func(hash tlb.Bits256) (*boc.Cell, error) {
cell, ok := cellsMap[hash]
if ok {
return cell, nil
}
return nil, errors.New("not found")
})
}
err = decoder.Unmarshal(proofCells[1], &stateProof)
if err != nil {
return nil, err
}
if stateProof.Proof.VirtualHash != *newStateHash {
return nil, errors.New("invalid virtual hash")
}
return &stateProof.Proof.VirtualRoot, nil
}

func checkBlockProof(proof *boc.Cell, blockRootHash ton.Bits256) (*tlb.Bits256, error) {
var res tlb.MerkleProof[tlb.Block]
err := tlb.Unmarshal(proof, &res) // merkle hash and depth checks inside
if err != nil {
return nil, fmt.Errorf("failed to unmarshal block proof: %w", err)
}
if ton.Bits256(res.VirtualHash) != blockRootHash {
return nil, fmt.Errorf("invalid block root hash")
}
return &res.VirtualRoot.StateUpdate.ToHash, nil // return new_hash field of MerkleUpdate of ShardState
}
171 changes: 171 additions & 0 deletions liteapi/account_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package liteapi

import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"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
accountID string
state string
proof string
}{
{
name: "account from masterchain",
accountID: "-1:34517c7bdf5187c55af4f8b61fdc321588c7ab768dee24b006df29106458d7cf",
state: "te6ccgEBAQEANwAAac/zRRfHvfUYfFWvT4th/cMhWIx6t2je4ksAbfKRBkWNfPICV8MiQ7WQAAAnwcjbgQjD0JAE",
proof: "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",
},
{
name: "active account from basechain",
accountID: "0:e33ed33a42eb2032059f97d90c706f8400bb256d32139ca707f1564ad699c7dd",
state: "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",
proof: "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",
},
}
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)
}
state, err := base64.StdEncoding.DecodeString(tt.state)
if err != nil {
t.Fatal("base64 decoding failed: %w", err)
}
proof, err := base64.StdEncoding.DecodeString(tt.proof)
if err != nil {
t.Fatal("base64 decoding failed: %w", err)
}
stateCells, err := boc.DeserializeBoc(state)
if err != nil {
t.Fatal("DeserializeBoc() failed: %w", err)
}
proofCells, err := boc.DeserializeBoc(proof)
if err != nil {
t.Fatal("DeserializeBoc() failed: %w", err)
}
hash, err := stateCells[0].Hash256()
if err != nil {
t.Fatal("Get hash failed: %w", err)
}
cellsMap := map[[32]byte]*boc.Cell{hash: stateCells[0]}
if err != nil {
t.Fatal("Get NonPrunedCells() failed: %w", err)
}
decoder := tlb.NewDecoder().WithDebug().WithPrunedResolver(func(hash tlb.Bits256) (*boc.Cell, error) {
cell, ok := cellsMap[hash]
if ok {
return cell, nil
}
return nil, errors.New("not found")
})
var stateProof struct {
Proof tlb.MerkleProof[tlb.ShardStateUnsplit]
}
err = decoder.Unmarshal(proofCells[1], &stateProof)
if err != nil {
t.Fatal("proof unmarshalling failed: %w", err)
}
values := stateProof.Proof.VirtualRoot.ShardStateUnsplit.Accounts.Values()
keys := stateProof.Proof.VirtualRoot.ShardStateUnsplit.Accounts.Keys()
for i, k := range keys {
if bytes.Equal(k[:], accountID.Address[:]) {
fmt.Printf("Account status: %v\n", values[i].Account.Status())
}
}
})
}
}

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)
})
}
}
3 changes: 3 additions & 0 deletions liteapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,9 @@ func (c *Client) GetAccountStateRaw(ctx context.Context, accountID ton.AccountID
if err != nil {
return liteclient.LiteServerAccountStateC{}, err
}
if res.Id.ToBlockIdExt() != blockID {
return liteclient.LiteServerAccountStateC{}, errors.New("invalid block ID")
}
return res, nil
}

Expand Down
7 changes: 7 additions & 0 deletions tlb/bintree.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ func (b *BinTree[T]) UnmarshalTLB(c *boc.Cell, decoder *Decoder) error {
}
b.Values = make([]T, 0, len(dec))
for _, i := range dec {
if i.CellType() == boc.PrunedBranchCell {
cell := resolvePrunedCell(c, decoder.resolvePruned)
if cell == nil {
continue
}
i = cell
}
var t T
err := decoder.Unmarshal(i, &t)
if err != nil {
Expand Down
Loading
Loading