diff --git a/contracts/contracts/examples/ticker.tolk b/contracts/contracts/examples/ticker.tolk new file mode 100644 index 000000000..099644101 --- /dev/null +++ b/contracts/contracts/examples/ticker.tolk @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +tolk 1.2 + +/// This Contract just forwards a message to itself, decreasing its value by 1 each time, until it reaches 0. It is used to mock a long chain of messages. + +import "@stdlib/common.tolk" +import "../lib/utils" +import "../lib/access/ownable_2step" + +const CONTRACT_VERSION = "1.0.0"; + +/// Message to start the ticker. +struct (0xd4834e00) Ticker_Tick { + queryID: uint64 // Standard query_id field + times: uint32 // Number of ticks to perform before stopping. +} + +struct Storage { + id: uint32 +} + +fun Storage.load(): Storage { + return Storage.fromCell(contract.getData()); +} + +fun Storage.store(self) { + return contract.setData(self.toCell()); +} + +type Msg = Ticker_Tick + +fun onInternalMessage(in: InMessage) { + val msg = lazy Msg.fromSlice(in.body); // 63 error code is thrown if the message opcode is unknown + match (msg) { + Ticker_Tick => { + if (msg.times == 0) { // We leave negative range out intentionally to allow for unbounded loops if needed + return; + } + + // send tick to itself + val reply = createMessage({ + bounce: false, + value: 0, + dest: contract.getAddress(), + body: Ticker_Tick { + queryID: msg.queryID, + times: msg.times - 1, + }, + }); + reply.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); + } + + else => { + assert (in.body.isEmpty()) throw 0xFFFF; + } + } +} +/// Gets the current id of the contract. +get fun id(): int { + val s = lazy Storage.load(); + return s.id; +} + +/// Gets the current type and version of the contract. +get fun typeAndVersion(): (slice, slice) { + return ("com.chainlink.ton.examples.Ticker", CONTRACT_VERSION) +} diff --git a/contracts/wrappers/examples.Ticker.compile.ts b/contracts/wrappers/examples.Ticker.compile.ts new file mode 100644 index 000000000..169b75b70 --- /dev/null +++ b/contracts/wrappers/examples.Ticker.compile.ts @@ -0,0 +1,8 @@ +import { CompilerConfig } from '@ton/blueprint' + +export const compile: CompilerConfig = { + lang: 'tolk', + entrypoint: 'contracts/examples/ticker.tolk', + withStackComments: true, // Fift output will contain comments, if you wish to debug its output + experimentalOptions: '', // you can pass experimental compiler options here +} diff --git a/integration-tests/tracetracking/integration_test.go b/integration-tests/tracetracking/integration_test.go index b17c7dec8..8fcd6b259 100644 --- a/integration-tests/tracetracking/integration_test.go +++ b/integration-tests/tracetracking/integration_test.go @@ -1,10 +1,15 @@ package tracetracking import ( + "context" + "fmt" "math/big" "math/rand/v2" + "slices" + "strings" "sync" "testing" + "time" chainsel "github.com/smartcontractkit/chain-selectors" @@ -12,6 +17,8 @@ import ( "github.com/stretchr/testify/require" "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/ton/wallet" "github.com/smartcontractkit/chainlink-ton/integration-tests/tracetracking/async/wrappers/requestreply" "github.com/smartcontractkit/chainlink-ton/integration-tests/tracetracking/async/wrappers/requestreplywithtwodependencies" @@ -22,6 +29,7 @@ import ( "github.com/smartcontractkit/chainlink-ton/pkg/bindings" "github.com/smartcontractkit/chainlink-ton/pkg/bindings/examples/counter" + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/examples/ticker" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/ownable2step" "github.com/smartcontractkit/chainlink-ton/pkg/ton/tracetracking" "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" @@ -620,4 +628,175 @@ func TestIntegration(t *testing.T) { t.Logf("Test completed successfully\n") }) + + t.Run("Ticker Delay", func(t *testing.T) { + const startingNumberOfTicks uint64 = 0 + const maxNumberOfTicks uint64 = 1000 + const step uint64 = 50 + const numWorkers = 10 + const measurementCount = (maxNumberOfTicks-startingNumberOfTicks)/step + 1 + + costPerTick := tlb.MustFromTON("0.01") + baseCost := tlb.MustFromTON("0.05") + + t.Parallel() + + t.Logf("\n\n\n\n\n\nTest Setup\n==========================\n") + + // Deploy pool of ticker contracts, each with its own deployer account + buildPath := bindings.GetBuildDir("examples.Ticker.compiled.json") + compiledContract, err := wrappers.ParseCompiledContract(buildPath) + require.NoError(t, err, "failed to parse compiled contract: %w", err) + + type worker struct { + deployer tracetracking.SignedAPIClient + tickerContract *address.Address + } + + deployers := make([]tracetracking.SignedAPIClient, numWorkers) + for i := range numWorkers { + deployer := getAccount() + deployers[i] = deployer + } + + workersCollector := make(chan worker, numWorkers) + var initWg sync.WaitGroup + for i, deployer := range deployers { + initWg.Add(1) + go func(i int) { + defer initWg.Done() + storage, err := tlb.ToCell(ticker.Storage{ + ID: getNextID(), + }) + require.NoError(t, err, "failed to serialize storage: %w", err) + tickerContract, _, err := wrappers.Deploy(t.Context(), &deployer, compiledContract, storage, tlb.MustFromTON("0.05"), tvm.EmptyCell) + require.NoError(t, err, "failed to deploy Ticker contract: %w", err) + workersCollector <- worker{ + deployer: deployer, + tickerContract: tickerContract.Address, + } + t.Logf("Ticker contract %d deployed at %s\n", i, tickerContract.Address.String()) + }(i) + } + + initWg.Wait() + close(workersCollector) + workers := make([]worker, 0, numWorkers) + for worker := range workersCollector { + workers = append(workers, worker) + } + + t.Logf("\n\n\n\n\n\nTest Started\n==========================\n") + + type measurement struct { + ticks uint64 + duration time.Duration + cost *tlb.Coins + } + + // Thread-safe measurements map + var measurementsLock sync.Mutex + measurements := make(map[uint64]measurement, measurementCount) + + // Error channel for worker failures + errChan := make(chan error, numWorkers) + + // Create task channel + tasks := make(chan uint64, measurementCount) + for numberOfTicks := startingNumberOfTicks; numberOfTicks <= maxNumberOfTicks; numberOfTicks += step { + tasks <- numberOfTicks + } + close(tasks) + + // Worker pool + var wg sync.WaitGroup + for workerID, w := range workers { + wg.Add(1) + go func(workerID int, w worker) { + defer wg.Done() + innerMeasurements := make([]measurement, 0, measurementCount) + for numberOfTicks := range tasks { + log := func(format string, args ...any) { + errChan <- fmt.Errorf("worker %d, task %d: %s", workerID, numberOfTicks, fmt.Sprintf(format, args...)) + } + t.Logf("Worker %d processing %d ticks\n", workerID, numberOfTicks) + + statingBalance := getBalance(t.Context(), t, w.deployer.Client, *w.tickerContract) + amountToSend := must(must(costPerTick.Mul(big.NewInt(int64(numberOfTicks)))).Add(&baseCost)) + startTime := time.Now() + trace, err := w.deployer.SendAndWaitForTrace(t.Context(), *w.tickerContract, &wallet.Message{ + Mode: wallet.PayGasSeparately, + InternalMessage: &tlb.InternalMessage{ + DstAddr: w.tickerContract, + Amount: *amountToSend, + Body: must(tlb.ToCell(ticker.Tick{ + QueryID: numberOfTicks, + Times: uint32(numberOfTicks), + })), + }, + }) + duration := time.Since(startTime) + if err != nil { + log("failed to send Tick message: %w", err) + return + } + ec, err := trace.TraceExitCode() + if err != nil { + log("failed to get exit code from trace: %w", err) + return + } + if ec != tvm.ExitCodeSuccess { + log("expected exit code 0, got %d", ec) + return + } + endBalance := getBalance(t.Context(), t, w.deployer.Client, *w.tickerContract) + cost := must(must(statingBalance.Add(amountToSend)).Sub(&endBalance)) + + innerMeasurements = append(innerMeasurements, measurement{numberOfTicks, duration, cost}) + + t.Logf("Worker %d completed %d ticks in %v\n", workerID, numberOfTicks, duration) + } + t.Logf("Worker %d finished processing\n", workerID) + + measurementsLock.Lock() + for _, measurement := range innerMeasurements { + measurements[measurement.ticks] = measurement + } + measurementsLock.Unlock() + t.Logf("Worker %d merged measurements\n", workerID) + }(workerID, w) + } + + wg.Wait() + close(errChan) + + // Check for any errors from workers + for err := range errChan { + require.NoError(t, err) + } + + var measurementsCSV strings.Builder + measurementsCSV.WriteString("number_of_ticks,duration_s,cost\n") + + // Sort measurements by number of ticks + sortedTicks := make([]uint64, 0, len(measurements)) + for numberOfTicks := range measurements { + sortedTicks = append(sortedTicks, numberOfTicks) + } + slices.Sort(sortedTicks) + + for _, numberOfTicks := range sortedTicks { + measurement := measurements[numberOfTicks] + fmt.Fprintf(&measurementsCSV, "%d,%.3f,%s\n", numberOfTicks, measurement.duration.Seconds(), measurement.cost.String()) + } + t.Logf("Measurements:\n%s", measurementsCSV.String()) + }) +} + +func getBalance(ctx context.Context, t *testing.T, client ton.APIClientWrapped, addr address.Address) tlb.Coins { + block, err := client.CurrentMasterchainInfo(ctx) + require.NoError(t, err, "failed to get masterchain info: %w", err) + account, err := client.GetAccount(ctx, block, &addr) + require.NoError(t, err, "failed to get account: %w", err) + return account.State.Balance } diff --git a/integration-tests/tracetracking/ticker.csv b/integration-tests/tracetracking/ticker.csv new file mode 100644 index 000000000..5fe148198 --- /dev/null +++ b/integration-tests/tracetracking/ticker.csv @@ -0,0 +1,22 @@ +number_of_ticks,duration_ms,cost +50,5.302,0.147950001 +550,24.484,1.617450003 +250,7.316,0.735750003 +750,41.098,2.205250004 +800,46.251,2.352200003 +350,9.356,1.029650003 +850,51.968,2.499150003 +0,5.043,0.001000001 +100,5.811,0.294900002 +600,27.744,1.764400003 +150,6.047,0.441850002 +650,32.649,1.911350004 +200,6.558,0.588800002 +400,11.005,1.176600003 +450,12.821,1.323550003 +1000,66.474,2.940000003 +300,8.312,0.882700003 +900,57.372,2.646100002 +950,64.006,2.793050002 +500,21.438,1.470500003 +700,36.276,2.058300004 \ No newline at end of file diff --git a/pkg/bindings/examples/ticker/ticker.go b/pkg/bindings/examples/ticker/ticker.go new file mode 100644 index 000000000..703b0af86 --- /dev/null +++ b/pkg/bindings/examples/ticker/ticker.go @@ -0,0 +1,21 @@ +package ticker + +import ( + "github.com/xssnick/tonutils-go/tlb" + + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +type Storage struct { + ID uint32 `tlb:"## 32"` +} + +type Tick struct { + _ tlb.Magic `tlb:"#d4834e00" json:"-"` //nolint:revive // Ignore opcode tag + QueryID uint64 `tlb:"## 64"` + Times uint32 `tlb:"## 32"` // Number of ticks to perform before stopping. +} + +var TLBs = tvm.MustNewTLBMap([]any{ + Tick{}, +}).MustWithStorageType(Storage{}) diff --git a/pkg/bindings/index.go b/pkg/bindings/index.go index 561a3a266..dc5be16a8 100644 --- a/pkg/bindings/index.go +++ b/pkg/bindings/index.go @@ -1,6 +1,7 @@ package bindings import ( + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/examples/ticker" "github.com/smartcontractkit/chainlink-ton/pkg/bindings/jetton/minter" "github.com/smartcontractkit/chainlink-ton/pkg/bindings/jetton/wallet" "github.com/smartcontractkit/chainlink-ton/pkg/bindings/lib/access/rbac" @@ -50,4 +51,7 @@ var Registry = tvm.ContractTLBRegistry{ PkgLib + ".access.RBAC": rbac.TLBs, PkgLib + ".funding.Withdrawable": withdrawable.TLBs, PkgLib + ".versioning.Upgradeable": upgradeable.TLBs, + + // Examples + PkgLib + ".examples.Ticker": ticker.TLBs, // Not sure if this should be here } diff --git a/pkg/ton/codec/debug/example/ticker/ticker.go b/pkg/ton/codec/debug/example/ticker/ticker.go new file mode 100644 index 000000000..8771125b4 --- /dev/null +++ b/pkg/ton/codec/debug/example/ticker/ticker.go @@ -0,0 +1,48 @@ +package ticker + +import ( + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/smartcontractkit/chainlink-ton/pkg/bindings/examples/ticker" + "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/router" + + "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/codec/debug/lib" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" +) + +var TLBs = ticker.TLBs + +type decoder struct { + tlbsCtx tvm.TLBMap +} + +func NewDecoder(tlbsCtx tvm.TLBMap) lib.ContractDecoder { + return &decoder{tlbsCtx} +} + +func (d *decoder) ContractType() string { + return "com.chainlink.ton.examples.Ticker" +} + +func (d *decoder) EventInfo(dstAddr *address.Address, msg *cell.Cell) (lib.MessageInfo, error) { + return nil, codec.ErrUnknownMessage +} + +func (d *decoder) ExternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { + return nil, codec.ErrUnknownMessage +} + +func (d *decoder) InternalMessageInfo(msg *cell.Cell) (lib.MessageInfo, error) { + return lib.NewMessageInfoFromCell(d.ContractType(), msg, TLBs, d.tlbsCtx) +} + +func (d *decoder) ExitCodeInfo(exitCode tvm.ExitCode) (string, error) { + ec, err := router.ExitCodeCodec.NewFrom(exitCode) + if err != nil { + return "", codec.ErrUnknownMessage + } + + return ec.String(), nil +}