From bfb9f2419974b9f81c9fa20e9acbc9f1653842d2 Mon Sep 17 00:00:00 2001 From: Denis Subbotin Date: Thu, 28 May 2026 17:01:20 +0300 Subject: [PATCH 1/3] storing mc_block in data for proof check --- blockchain/blockchain.go | 26 +++++++++++-------- blockchain/proofs.go | 29 +++++++++++++++++++++ cmd/processor/main.go | 21 ++++++++-------- db/proofs.go | 35 ++++++++++++++++++++++++++ deploy/db/02_add_proofs_table.down.sql | 1 + deploy/db/02_add_proofs_table.up.sql | 8 ++++++ 6 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 blockchain/proofs.go create mode 100644 db/proofs.go create mode 100644 deploy/db/02_add_proofs_table.down.sql create mode 100644 deploy/db/02_add_proofs_table.up.sql diff --git a/blockchain/blockchain.go b/blockchain/blockchain.go index 8c5f915..c7bcf5f 100644 --- a/blockchain/blockchain.go +++ b/blockchain/blockchain.go @@ -87,7 +87,7 @@ func retry2[T1 any, T2 any](fn func() (T1, T2, error)) (T1, T2, error) { } // NewConnection creates new Blockchain connection -func NewConnection(addr, key string, rateLimit int) (*Connection, error) { +func NewConnection(addr, key string, rateLimit int, blockStorage provenBlocksStorage) (*Connection, error) { client := liteclient.NewConnectionPool() ctx, cancel := context.WithTimeout(context.Background(), time.Second*120) @@ -103,26 +103,30 @@ func NewConnection(addr, key string, rateLimit int) (*Connection, error) { var wrappedClient ton.APIClientWrapped if config.Config.ProofCheckEnabled { + wrappedClient = ton.NewAPIClient(limitedClient, ton.ProofCheckPolicySecure) - if config.Config.NetworkConfigUrl == "" { - return nil, fmt.Errorf("empty network config URL") - } + lastBlock, err := blockStorage.GetLastMasterchainProvenBlock(ctx) + if err == nil { + wrappedClient.SetTrustedBlock(lastBlock) + } else { + if config.Config.NetworkConfigUrl == "" { + return nil, fmt.Errorf("empty network config URL") + } - cfg, err := liteclient.GetConfigFromUrl(ctx, config.Config.NetworkConfigUrl) - if err != nil { - return nil, fmt.Errorf("get network config from url err: %s", err.Error()) + cfg, err := liteclient.GetConfigFromUrl(ctx, config.Config.NetworkConfigUrl) + if err != nil { + return nil, fmt.Errorf("get network config from url err: %s", err.Error()) + } + wrappedClient.SetTrustedBlockFromConfig(cfg) } - wrappedClient = ton.NewAPIClient(limitedClient, ton.ProofCheckPolicySecure) - wrappedClient.SetTrustedBlockFromConfig(cfg) - log.Infof("Fetching and checking proofs since config init block ...") _, err = wrappedClient.CurrentMasterchainInfo(ctx) // we fetch block just to trigger chain proof check if err != nil { return nil, fmt.Errorf("get masterchain info err: %s", err.Error()) } log.Infof("Proof checks are completed") - + go updateLastBlocks(wrappedClient, blockStorage) } else { wrappedClient = ton.NewAPIClient(limitedClient, ton.ProofCheckPolicyUnsafe) } diff --git a/blockchain/proofs.go b/blockchain/proofs.go new file mode 100644 index 0000000..51236fa --- /dev/null +++ b/blockchain/proofs.go @@ -0,0 +1,29 @@ +package blockchain + +import ( + "context" + "time" + + log "github.com/sirupsen/logrus" + "github.com/xssnick/tonutils-go/ton" +) + +type provenBlocksStorage interface { + SaveLastMasterchainProvenBlock(ctx context.Context, block ton.BlockIDExt) error + GetLastMasterchainProvenBlock(ctx context.Context) (*ton.BlockIDExt, error) +} + +func updateLastBlocks(client ton.APIClientWrapped, storage provenBlocksStorage) { + for { + time.Sleep(time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + last, err := client.GetMasterchainInfo(ctx) + if err == nil { + err := storage.SaveLastMasterchainProvenBlock(ctx, *last) + if err != nil { + log.Errorf("save last masterchain block err: %v. If you are using PROOF_CHECK_ENABLED please make migration 02_add_proofs_table.up.sql ", err) + } + } + cancel() + } +} diff --git a/cmd/processor/main.go b/cmd/processor/main.go index a185e06..f767004 100644 --- a/cmd/processor/main.go +++ b/cmd/processor/main.go @@ -4,6 +4,13 @@ import ( "context" "errors" "fmt" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + "github.com/gobicycle/bicycle/api" "github.com/gobicycle/bicycle/blockchain" "github.com/gobicycle/bicycle/config" @@ -12,12 +19,6 @@ import ( "github.com/gobicycle/bicycle/queue" "github.com/gobicycle/bicycle/webhook" log "github.com/sirupsen/logrus" - "net/http" - "os" - "os/signal" - "sync" - "syscall" - "time" ) var Version = "dev" @@ -32,14 +33,14 @@ func main() { signal.Notify(sigChannel, os.Interrupt, syscall.SIGTERM) wg := new(sync.WaitGroup) - bcClient, err := blockchain.NewConnection(config.Config.LiteServer, config.Config.LiteServerKey, config.Config.LiteServerRateLimit) + dbClient, err := db.NewConnection(config.Config.DatabaseURI) if err != nil { - log.Fatalf("blockchain connection error: %v", err) + log.Fatalf("DB connection error: %v", err) } - dbClient, err := db.NewConnection(config.Config.DatabaseURI) + bcClient, err := blockchain.NewConnection(config.Config.LiteServer, config.Config.LiteServerKey, config.Config.LiteServerRateLimit, dbClient) if err != nil { - log.Fatalf("DB connection error: %v", err) + log.Fatalf("blockchain connection error: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), time.Second*120) diff --git a/db/proofs.go b/db/proofs.go new file mode 100644 index 0000000..c975597 --- /dev/null +++ b/db/proofs.go @@ -0,0 +1,35 @@ +package db + +import ( + "context" + + "github.com/xssnick/tonutils-go/ton" +) + +func (c *Connection) SaveLastMasterchainProvenBlock(ctx context.Context, block ton.BlockIDExt) error { + _, err := c.client.Exec(ctx, ` + INSERT INTO payments.last_proven_block ( + slug, + workchain, + shard, + seqno, + root_hash, + file_hash + ) VALUES ('head', $1, $2, $3, $4, $5) + ON CONFLICT (slug) DO UPDATE SET workchain = $1, shard = $2, seqno = $3, root_hash = $4, file_hash = $5 + `, block.Workchain, + block.Shard, + block.SeqNo, + block.RootHash, + block.FileHash, + ) + return err +} + +func (c *Connection) GetLastMasterchainProvenBlock(ctx context.Context) (*ton.BlockIDExt, error) { + var block ton.BlockIDExt + err := c.client. + QueryRow(ctx, `SELECT workchain, shard, seqno, root_hash, file_hash FROM payments.last_proven_block WHERE slug = 'head'`). + Scan(&block.Workchain, &block.Shard, &block.SeqNo, &block.RootHash, &block.FileHash) + return &block, err +} diff --git a/deploy/db/02_add_proofs_table.down.sql b/deploy/db/02_add_proofs_table.down.sql new file mode 100644 index 0000000..369d54c --- /dev/null +++ b/deploy/db/02_add_proofs_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS payments.last_proven_block; \ No newline at end of file diff --git a/deploy/db/02_add_proofs_table.up.sql b/deploy/db/02_add_proofs_table.up.sql new file mode 100644 index 0000000..405e706 --- /dev/null +++ b/deploy/db/02_add_proofs_table.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS payments.last_proven_block ( + slug text PRIMARY KEY, + workchain bigint not null, + shard bigint not null, + seqno bigint not null, + root_hash bytea not null, + file_hash bytea not null +); \ No newline at end of file From aa1a64fb85a408d4119c94d2700a277eebda755e Mon Sep 17 00:00:00 2001 From: Denis Subbotin Date: Thu, 28 May 2026 20:28:16 +0300 Subject: [PATCH 2/3] fix tests and improve migrations documentation --- blockchain/blockchain_test.go | 30 +++++++++++++++---- blockchain/proofs.go | 4 +-- deploy/db/01_init.down.sql | 1 + deploy/db/01_init.up.sql | 9 ++++++ deploy/db/02_add_proofs_table.down.sql | 1 - .../0.10.x-0.11.0.sql} | 0 manual_migrations.md | 15 ++++++++++ 7 files changed, 51 insertions(+), 9 deletions(-) delete mode 100644 deploy/db/02_add_proofs_table.down.sql rename deploy/{db/02_add_proofs_table.up.sql => manual_migrations/0.10.x-0.11.0.sql} (100%) diff --git a/blockchain/blockchain_test.go b/blockchain/blockchain_test.go index f68c2cc..4b3334f 100644 --- a/blockchain/blockchain_test.go +++ b/blockchain/blockchain_test.go @@ -3,16 +3,18 @@ package blockchain import ( "bytes" "context" - "github.com/gobicycle/bicycle/core" - "github.com/xssnick/tonutils-go/address" - "github.com/xssnick/tonutils-go/tlb" - "github.com/xssnick/tonutils-go/ton/jetton" - "github.com/xssnick/tonutils-go/ton/wallet" "math/big" "math/rand" "os" "testing" "time" + + "github.com/gobicycle/bicycle/core" + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/ton/jetton" + "github.com/xssnick/tonutils-go/ton/wallet" ) var ( @@ -21,6 +23,22 @@ var ( notActiveAccount, _ = address.ParseAddr("kQAkRRJ1RiViVHY2UmUhWCFjdiZBeEYnhkhxI1JTJFNUNG9v") ) +type mockBlockStorage struct{} + +func (m mockBlockStorage) SaveLastMasterchainProvenBlock(ctx context.Context, block ton.BlockIDExt) error { + return nil +} + +func (m mockBlockStorage) GetLastMasterchainProvenBlock(ctx context.Context) (*ton.BlockIDExt, error) { + return &ton.BlockIDExt{ + Workchain: -1, + Shard: -8000000000000000, + SeqNo: 69722697, + RootHash: []byte{0x4b, 0x7e, 0x27, 0xd3, 0xcc, 0x60, 0xbf, 0x16, 0xc7, 0x1b, 0xe0, 0x2d, 0xe8, 0xee, 0xb1, 0xb8, 0x74, 0xc9, 0x1d, 0xe6, 0x49, 0xe6, 0x2f, 0x3a, 0xd0, 0x88, 0xd5, 0xeb, 0x7a, 0x6a, 0x8d, 0x30}, + FileHash: []byte{0x41, 0x89, 0x69, 0xd0, 0x94, 0x1d, 0x94, 0x63, 0xa1, 0x58, 0xbf, 0xfe, 0x27, 0x38, 0x52, 0xe3, 0x4b, 0x35, 0x13, 0x4a, 0x38, 0xde, 0xf0, 0x26, 0x90, 0xec, 0xc8, 0x95, 0xb5, 0x10, 0xba, 0xa8}, + }, nil +} + func connect(t *testing.T) *Connection { server := os.Getenv("SERVER") if server == "" { @@ -30,7 +48,7 @@ func connect(t *testing.T) *Connection { if key == "" { t.Fatal("empty key var") } - c, err := NewConnection(server, key, 100) + c, err := NewConnection(server, key, 100, mockBlockStorage{}) if err != nil { t.Fatal("connections err: ", err) } diff --git a/blockchain/proofs.go b/blockchain/proofs.go index 51236fa..67f044b 100644 --- a/blockchain/proofs.go +++ b/blockchain/proofs.go @@ -15,15 +15,15 @@ type provenBlocksStorage interface { func updateLastBlocks(client ton.APIClientWrapped, storage provenBlocksStorage) { for { - time.Sleep(time.Minute) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) last, err := client.GetMasterchainInfo(ctx) if err == nil { err := storage.SaveLastMasterchainProvenBlock(ctx, *last) if err != nil { - log.Errorf("save last masterchain block err: %v. If you are using PROOF_CHECK_ENABLED please make migration 02_add_proofs_table.up.sql ", err) + log.Errorf("save last masterchain block err: %v. If you are using PROOF_CHECK_ENABLED please make migration 0.10.x-0.11.0.sql ", err) } } cancel() + time.Sleep(time.Minute * 10) } } diff --git a/deploy/db/01_init.down.sql b/deploy/db/01_init.down.sql index 54c45fd..ba23728 100644 --- a/deploy/db/01_init.down.sql +++ b/deploy/db/01_init.down.sql @@ -9,6 +9,7 @@ DROP TABLE IF EXISTS payments.external_incomes; DROP TABLE IF EXISTS payments.block_data; DROP TABLE IF EXISTS payments.internal_withdrawals; DROP TABLE IF EXISTS payments.service_withdrawal_requests; +DROP TABLE IF EXISTS payments.last_proven_block; DROP SCHEMA IF EXISTS payments; diff --git a/deploy/db/01_init.up.sql b/deploy/db/01_init.up.sql index 4bf2cdd..6aede80 100644 --- a/deploy/db/01_init.up.sql +++ b/deploy/db/01_init.up.sql @@ -167,4 +167,13 @@ CREATE TABLE IF NOT EXISTS payments.service_withdrawal_requests jetton_master bytea ); +CREATE TABLE IF NOT EXISTS payments.last_proven_block ( + slug text PRIMARY KEY, + workchain bigint not null, + shard bigint not null, + seqno bigint not null, + root_hash bytea not null, + file_hash bytea not null +); + COMMIT; \ No newline at end of file diff --git a/deploy/db/02_add_proofs_table.down.sql b/deploy/db/02_add_proofs_table.down.sql deleted file mode 100644 index 369d54c..0000000 --- a/deploy/db/02_add_proofs_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS payments.last_proven_block; \ No newline at end of file diff --git a/deploy/db/02_add_proofs_table.up.sql b/deploy/manual_migrations/0.10.x-0.11.0.sql similarity index 100% rename from deploy/db/02_add_proofs_table.up.sql rename to deploy/manual_migrations/0.10.x-0.11.0.sql diff --git a/manual_migrations.md b/manual_migrations.md index 64d96a5..a8606fb 100644 --- a/manual_migrations.md +++ b/manual_migrations.md @@ -12,3 +12,18 @@ will be filled with a 0 workchain. 2. Build new docker image and recreate container for `payment-processor` as described in `Service deploy` chapter in [Readme](/README.md) Note that this query creates a new nullable column in the `external_withdrawals` and `external_incomes` DB tables and `binary_comment` column in `withdrawal_requests` table. + +## 0.10.x -> 0.11.0 +(**Optional**. Recommended if you are using proofs checking (PROOF_CHECK_ENABLED=true)) +1. Apply [DB migration](/deploy/manual_migrations/0.10.x-0.11.0.sql) +2. Build new docker image and recreate container for `payment-processor` as described in `Service deploy` chapter in [Readme](/README.md) + +Note that this query creates a new table `payments.last_proven_block`. +It will be filled automatically based on config file (NETWORK_CONFIG_URL) and archive node, but you can skip requirements for an archive node and fill it manually: + +Go to https://tonviewer.com/last, take **seqno**, **root_hash** and **file_hash** and put them into the request below. +For example 69722697, 4b7e27d3cc60bf16c71be02de8eeb1b874c91de649e62f3ad088d5eb7a6a8d30, 418969d0941d9463a158bffe273852e34b35134a38def02690ecc895b510baa8 +```sql +INSERT INTO payments.last_proven_block ( slug, workchain, shard, seqno, root_hash, file_hash) +VALUES ('head', -1, -9223372036854775808, 69722697, E'\\x4b7e27d3cc60bf16c71be02de8eeb1b874c91de649e62f3ad088d5eb7a6a8d30', E'\\x418969d0941d9463a158bffe273852e34b35134a38def02690ecc895b510baa8') +``` \ No newline at end of file From 91a2ad26a367d5fc624995f38ddbbb8f115159ad Mon Sep 17 00:00:00 2001 From: GoBicycle <120649456+gobicycle@users.noreply.github.com> Date: Thu, 28 May 2026 21:42:44 +0300 Subject: [PATCH 3/3] Update main.go --- cmd/testutil/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/testutil/main.go b/cmd/testutil/main.go index 3342bb6..67ad9fa 100644 --- a/cmd/testutil/main.go +++ b/cmd/testutil/main.go @@ -53,7 +53,7 @@ func main() { log.Fatalf("invalid HOT_WALLET_B env var") } - bcClient, err := blockchain.NewConnection(config.Config.LiteServer, config.Config.LiteServerKey, config.Config.LiteServerRateLimit) + bcClient, err := blockchain.NewConnection(config.Config.LiteServer, config.Config.LiteServerKey, config.Config.LiteServerRateLimit, nil) if err != nil { log.Fatalf("blockchain connection error: %v", err) }