Skip to content

Commit 85577de

Browse files
authored
Merge pull request #7493 from devbugging/gregor/callbacks/02-blueprint-implementation
[Scheduled Callbacks] Blueprints and transaction scripts
2 parents 73569b4 + e9cad95 commit 85577de

File tree

4 files changed

+338
-0
lines changed

4 files changed

+338
-0
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package blueprints
2+
3+
import (
4+
_ "embed"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/onflow/cadence"
9+
"github.com/onflow/cadence/encoding/ccf"
10+
11+
"github.com/onflow/flow-go/model/flow"
12+
)
13+
14+
// processScheduledCallbacksTransaction calls scheduled callback contract
15+
// and process new callbacks that should be executed.
16+
//
17+
//go:embed scripts/processScheduledCallbacksTransaction.cdc
18+
var processCallbacksTransaction string
19+
20+
// executeCallbacksTransaction calls scheduled callback contract
21+
// to execute the provided callback by ID.
22+
//
23+
//go:embed scripts/executeScheduledCallbackTransaction.cdc
24+
var executeCallbacksTransaction string
25+
26+
const (
27+
placeholderScheduledContract = "import \"CallbackScheduler\""
28+
processedCallbackIDFieldName = "ID"
29+
processedCallbackEffortFieldName = "executionEffort"
30+
processedEventTypeTemplate = "A.%v.CallbackScheduler.CallbackProcessed"
31+
)
32+
33+
func ProcessCallbacksTransaction(chain flow.Chain, maxEffortLeft uint64) (*flow.TransactionBody, error) {
34+
script := prepareScheduledContractTransaction(chain, processCallbacksTransaction)
35+
effort, err := ccf.Encode(cadence.UInt64(maxEffortLeft))
36+
if err != nil {
37+
return nil, fmt.Errorf("failed to encode max effort left: %w", err)
38+
}
39+
40+
tx := flow.NewTransactionBody().
41+
SetScript(script).
42+
AddArgument(effort).
43+
SetComputeLimit(flow.DefaultMaxTransactionGasLimit)
44+
45+
return tx, nil
46+
}
47+
48+
func ExecuteCallbacksTransactions(chainID flow.Chain, processEvents flow.EventsList) ([]*flow.TransactionBody, error) {
49+
txs := make([]*flow.TransactionBody, 0, len(processEvents))
50+
51+
for _, event := range processEvents {
52+
id, effort, err := callbackArgsFromEvent(event)
53+
if err != nil {
54+
return nil, fmt.Errorf("failed to get callback args from event: %w", err)
55+
}
56+
57+
tx := executeCallbackTransaction(chainID, id, effort)
58+
txs = append(txs, tx)
59+
}
60+
61+
return txs, nil
62+
}
63+
64+
func executeCallbackTransaction(chain flow.Chain, id []byte, effort uint64) *flow.TransactionBody {
65+
script := prepareScheduledContractTransaction(chain, executeCallbacksTransaction)
66+
return flow.NewTransactionBody().
67+
SetScript(script).
68+
AddArgument(id).
69+
SetComputeLimit(effort)
70+
}
71+
72+
// callbackArgsFromEvent decodes the event payload and returns the callback ID and effort.
73+
//
74+
// The event for processed callback event is emitted by the process callback transaction from
75+
// callback scheduler contract and has the following signature:
76+
// event CallbackProcessed(ID: UInt64, executionEffort: UInt64)
77+
func callbackArgsFromEvent(event flow.Event) ([]byte, uint64, error) {
78+
scheduledContractAddress := "0x0000000000000000" // todo use contract addr
79+
if string(event.Type) != fmt.Sprintf(processedEventTypeTemplate, scheduledContractAddress) {
80+
return nil, 0, fmt.Errorf("wrong event type is passed")
81+
}
82+
83+
eventData, err := ccf.Decode(nil, event.Payload)
84+
if err != nil {
85+
return nil, 0, fmt.Errorf("failed to decode event: %w", err)
86+
}
87+
88+
cadenceEvent, ok := eventData.(cadence.Event)
89+
if !ok {
90+
return nil, 0, fmt.Errorf("event data is not a cadence event")
91+
}
92+
93+
idValue := cadence.SearchFieldByName(
94+
cadenceEvent,
95+
processedCallbackIDFieldName,
96+
)
97+
98+
effortValue := cadence.SearchFieldByName(
99+
cadenceEvent,
100+
processedCallbackEffortFieldName,
101+
)
102+
103+
id, ok := idValue.(cadence.UInt64)
104+
if !ok {
105+
return nil, 0, fmt.Errorf("id is not uint64")
106+
}
107+
108+
effort, ok := effortValue.(cadence.UInt64)
109+
if !ok {
110+
return nil, 0, fmt.Errorf("effort is not uint64")
111+
}
112+
113+
encodedID, err := ccf.Encode(id)
114+
if err != nil {
115+
return nil, 0, fmt.Errorf("failed to encode id: %w", err)
116+
}
117+
118+
return encodedID, uint64(effort), nil
119+
}
120+
121+
func prepareScheduledContractTransaction(_ flow.Chain, txScript string) []byte {
122+
// todo use this instead of palceholder address
123+
// _ = systemcontracts.SystemContractsForChain(chain.ChainID())
124+
scheduledContractAddress := "0x0000000000000000"
125+
126+
code := strings.ReplaceAll(
127+
txScript,
128+
placeholderScheduledContract,
129+
fmt.Sprintf("%s from %s", placeholderScheduledContract, scheduledContractAddress),
130+
)
131+
132+
return []byte(code)
133+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package blueprints_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/onflow/cadence"
10+
cadenceCommon "github.com/onflow/cadence/common"
11+
"github.com/onflow/cadence/encoding/ccf"
12+
13+
"github.com/onflow/flow-go/fvm/blueprints"
14+
"github.com/onflow/flow-go/model/flow"
15+
"github.com/onflow/flow-go/utils/unittest"
16+
)
17+
18+
func TestProcessCallbacksTransaction(t *testing.T) {
19+
t.Parallel()
20+
21+
chain := flow.Mainnet.Chain()
22+
const effortLeft = 100
23+
tx, err := blueprints.ProcessCallbacksTransaction(chain, effortLeft)
24+
require.NoError(t, err)
25+
26+
assert.NotNil(t, tx)
27+
assert.NotEmpty(t, tx.Script)
28+
assert.Equal(t, uint64(flow.DefaultMaxTransactionGasLimit), tx.GasLimit)
29+
encodedEffort, err := ccf.Encode(cadence.UInt64(effortLeft))
30+
require.NoError(t, err)
31+
assert.Equal(t, encodedEffort, tx.Arguments[0])
32+
}
33+
34+
func TestExecuteCallbacksTransactions(t *testing.T) {
35+
t.Parallel()
36+
37+
chain := flow.Mainnet.Chain()
38+
39+
tests := []struct {
40+
name string
41+
events []flow.Event
42+
expectedTxs int
43+
expectError bool
44+
errorMessage string
45+
}{
46+
{
47+
name: "no events",
48+
events: []flow.Event{},
49+
expectedTxs: 0,
50+
expectError: false,
51+
},
52+
{
53+
name: "single valid event",
54+
events: []flow.Event{createValidCallbackEvent(t, 1, 100)},
55+
expectedTxs: 1,
56+
expectError: false,
57+
},
58+
{
59+
name: "multiple valid events",
60+
events: []flow.Event{
61+
createValidCallbackEvent(t, 1, 100),
62+
createValidCallbackEvent(t, 2, 200),
63+
createValidCallbackEvent(t, 3, 300),
64+
},
65+
expectedTxs: 3,
66+
expectError: false,
67+
},
68+
{
69+
name: "invalid event type",
70+
events: []flow.Event{createInvalidTypeEvent()},
71+
expectedTxs: 0,
72+
expectError: true,
73+
errorMessage: "failed to get callback args from event",
74+
},
75+
{
76+
name: "invalid event payload",
77+
events: []flow.Event{createInvalidPayloadEvent()},
78+
expectedTxs: 0,
79+
expectError: true,
80+
errorMessage: "failed to get callback args from event",
81+
},
82+
}
83+
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
txs, err := blueprints.ExecuteCallbacksTransactions(chain, tt.events)
87+
88+
if tt.expectError {
89+
assert.Error(t, err)
90+
assert.Contains(t, err.Error(), tt.errorMessage)
91+
assert.Nil(t, txs)
92+
return
93+
}
94+
95+
assert.NoError(t, err)
96+
assert.Len(t, txs, tt.expectedTxs)
97+
98+
for i, tx := range txs {
99+
assert.NotNil(t, tx)
100+
assert.NotEmpty(t, tx.Script)
101+
expectedEffort := uint64(100 * (i + 1)) // Events created with efforts 100, 200, 300
102+
assert.Equal(t, expectedEffort, tx.GasLimit)
103+
assert.Len(t, tx.Arguments, 1)
104+
assert.NotEmpty(t, tx.Arguments[0])
105+
106+
t.Logf("Transaction %d: ID arg length: %d, GasLimit: %d",
107+
i, len(tx.Arguments[0]), tx.GasLimit)
108+
}
109+
})
110+
}
111+
}
112+
113+
func TestExecuteCallbackTransaction(t *testing.T) {
114+
t.Parallel()
115+
116+
chain := flow.Mainnet.Chain()
117+
118+
const id = 123
119+
const effort = 456
120+
event := createValidCallbackEvent(t, id, effort)
121+
txs, err := blueprints.ExecuteCallbacksTransactions(chain, []flow.Event{event})
122+
123+
require.NoError(t, err)
124+
require.Len(t, txs, 1)
125+
126+
tx := txs[0]
127+
assert.NotNil(t, tx)
128+
assert.NotEmpty(t, tx.Script)
129+
assert.Equal(t, uint64(effort), tx.GasLimit)
130+
assert.Len(t, tx.Arguments, 1)
131+
132+
expectedEncodedID, err := ccf.Encode(cadence.NewUInt64(id))
133+
require.NoError(t, err)
134+
assert.Equal(t, tx.Arguments[0], expectedEncodedID)
135+
136+
assert.Equal(t, tx.GasLimit, uint64(effort))
137+
}
138+
139+
func createValidCallbackEvent(t *testing.T, id uint64, effort uint64) flow.Event {
140+
// todo use proper location
141+
contractAddress := flow.HexToAddress("0x0000000000000000")
142+
location := cadenceCommon.NewAddressLocation(nil, cadenceCommon.Address(contractAddress), "CallbackScheduler")
143+
144+
eventType := cadence.NewEventType(
145+
location,
146+
"CallbackProcessed",
147+
[]cadence.Field{
148+
{Identifier: "ID", Type: cadence.UInt64Type},
149+
{Identifier: "executionEffort", Type: cadence.UInt64Type},
150+
},
151+
nil,
152+
)
153+
154+
event := cadence.NewEvent(
155+
[]cadence.Value{
156+
cadence.NewUInt64(id),
157+
cadence.NewUInt64(effort),
158+
},
159+
).WithType(eventType)
160+
161+
payload, err := ccf.Encode(event)
162+
require.NoError(t, err)
163+
164+
return flow.Event{
165+
Type: flow.EventType("A.0x0000000000000000.CallbackScheduler.CallbackProcessed"),
166+
TransactionID: unittest.IdentifierFixture(),
167+
TransactionIndex: 0,
168+
EventIndex: 0,
169+
Payload: payload,
170+
}
171+
}
172+
173+
func createInvalidTypeEvent() flow.Event {
174+
return flow.Event{
175+
Type: flow.EventType("A.0x0000000000000000.SomeContract.WrongEvent"),
176+
TransactionID: unittest.IdentifierFixture(),
177+
TransactionIndex: 0,
178+
EventIndex: 0,
179+
Payload: []byte("invalid"),
180+
}
181+
}
182+
183+
func createInvalidPayloadEvent() flow.Event {
184+
return flow.Event{
185+
Type: flow.EventType("A.0x0000000000000000.CallbackScheduler.CallbackProcessed"),
186+
TransactionID: unittest.IdentifierFixture(),
187+
TransactionIndex: 0,
188+
EventIndex: 0,
189+
Payload: []byte("not valid ccf"),
190+
}
191+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import "CallbackScheduler"
2+
3+
transaction(callbackID: UInt64) {
4+
execute {
5+
CallbackScheduler.executeCallback(ID: callbackID)
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import "CallbackScheduler"
2+
3+
transaction(maxEffortLeft: UInt64) {
4+
execute {
5+
CallbackScheduler.process(maxEffortLeft: effortLimit)
6+
}
7+
}

0 commit comments

Comments
 (0)