Skip to content
Draft
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
67 changes: 67 additions & 0 deletions contracts/contracts/examples/ticker.tolk
Original file line number Diff line number Diff line change
@@ -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)
}
8 changes: 8 additions & 0 deletions contracts/wrappers/examples.Ticker.compile.ts
Original file line number Diff line number Diff line change
@@ -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
}
179 changes: 179 additions & 0 deletions integration-tests/tracetracking/integration_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package tracetracking

import (
"context"
"fmt"
"math/big"
"math/rand/v2"
"slices"
"strings"
"sync"
"testing"
"time"

chainsel "github.com/smartcontractkit/chain-selectors"

"github.com/stretchr/testify/assert"
"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"
Expand All @@ -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"
Expand Down Expand Up @@ -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
}
22 changes: 22 additions & 0 deletions integration-tests/tracetracking/ticker.csv
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions pkg/bindings/examples/ticker/ticker.go
Original file line number Diff line number Diff line change
@@ -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{})
4 changes: 4 additions & 0 deletions pkg/bindings/index.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
}
48 changes: 48 additions & 0 deletions pkg/ton/codec/debug/example/ticker/ticker.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading