Skip to content

Commit 95576a5

Browse files
committed
WIP: export/import test fails.
1 parent ba9de24 commit 95576a5

12 files changed

Lines changed: 272 additions & 194 deletions

cmd/sonictool/app/app_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ import (
3030

3131
sonictool "github.com/0xsoniclabs/sonic/cmd/sonictool/app"
3232
"github.com/0xsoniclabs/sonic/cmd/sonictool/genesis"
33+
"github.com/0xsoniclabs/sonic/ethapi"
3334
"github.com/0xsoniclabs/sonic/opera"
3435
ogenesis "github.com/0xsoniclabs/sonic/opera/genesis"
3536
"github.com/0xsoniclabs/sonic/opera/genesisstore"
3637
"github.com/0xsoniclabs/sonic/tests"
38+
"github.com/0xsoniclabs/sonic/tests/bundles"
3739
"github.com/0xsoniclabs/sonic/utils/caution"
3840
"github.com/0xsoniclabs/sonic/utils/prompt"
41+
"github.com/ethereum/go-ethereum"
3942
"github.com/ethereum/go-ethereum/common"
4043
"github.com/ethereum/go-ethereum/common/hexutil"
4144
"github.com/ethereum/go-ethereum/core/types"
@@ -231,6 +234,25 @@ func TestSonicTool_genesis_ExportsAndSigns_WithoutErrors(t *testing.T) {
231234
revertPrompt()
232235
}
233236

237+
func TestSonicTool_genesis_ExportImportBundles_PreservesExecutedBundle(t *testing.T) {
238+
upgrades := opera.GetBrioUpgrades()
239+
upgrades.TransactionBundles = true
240+
net := tests.StartIntegrationTestNet(t, tests.IntegrationTestNetOptions{Upgrades: &upgrades})
241+
242+
bundleHash, txs, plan := runBundle(t, net)
243+
244+
require.NoError(t, net.RestartWithExportImport())
245+
246+
newClient, err := net.GetClient()
247+
require.NoError(t, err)
248+
defer newClient.Close()
249+
250+
// try to submit the same bundle again, it should be recognized as already executed
251+
newBundleHash, err := bundles.SubmitBundle(newClient, txs, plan)
252+
require.ErrorContains(t, err, "bundle already executed")
253+
require.Equal(t, bundleHash, newBundleHash, "expected the same bundle hash after genesis export/import")
254+
}
255+
234256
func TestSonicTool_heal_ExecutesWithoutErrors(t *testing.T) {
235257
net := tests.StartIntegrationTestNet(
236258
t,
@@ -582,3 +604,51 @@ func replaceUserPrompter(newPrompt prompt.UserPrompter) (cleanup func()) {
582604
cleanup = func() { prompt.UserPrompt = oldPrompt }
583605
return
584606
}
607+
608+
// runBundle prepares and runs a bundle with a single transaction,
609+
// waits for its execution and returns the envelop transaction hash and the
610+
// execution plan hash.
611+
func runBundle(t *testing.T, net *tests.IntegrationTestNet) (
612+
common.Hash,
613+
[]*types.Transaction,
614+
ethapi.RPCExecutionPlan,
615+
) {
616+
sender := net.GetSessionSponsor()
617+
618+
client, err := net.GetClient()
619+
require.NoError(t, err)
620+
defer client.Close()
621+
622+
// prepare a bundle with a single transaction
623+
gasPrice, err := client.SuggestGasPrice(t.Context())
624+
require.NoError(t, err)
625+
626+
tx := ethereum.CallMsg{
627+
From: sender.Address(),
628+
To: &common.Address{0x42},
629+
GasPrice: gasPrice,
630+
}
631+
632+
earliest, err := client.BlockNumber(t.Context())
633+
require.NoError(t, err)
634+
latest := earliest + 10
635+
636+
preparedBundle, err := bundles.PrepareBundle(t, client, earliest, latest, []ethereum.CallMsg{tx})
637+
require.NoError(t, err)
638+
signer := types.LatestSignerForChainID(net.GetChainId())
639+
640+
txs := make([]*types.Transaction, len(preparedBundle.Transactions))
641+
for i, txArgs := range preparedBundle.Transactions {
642+
txs[i], err = types.SignTx(txArgs.ToTransaction(), signer, sender.PrivateKey)
643+
require.NoError(t, err)
644+
}
645+
646+
// Submit the bundle
647+
bundleHash, err := bundles.SubmitBundle(client, txs, preparedBundle.Plan)
648+
require.NoError(t, err)
649+
info, err := bundles.WaitForBundleExecution(t.Context(), client.Client(), bundleHash)
650+
require.NoError(t, err)
651+
require.Equal(t, ethapi.BundleStatusExecuted, info.Status)
652+
653+
return bundleHash, txs, preparedBundle.Plan
654+
}

gossip/apply_genesis.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func (s *Store) ApplyGenesis(g genesis.Genesis) (err error) {
151151

152152
bundleHistory := make(map[uint64][]bundle.ExecutionInfo)
153153
// write bundles
154-
g.ProcessedBundles.ForEach(func(info bundle.ExecutionInfo) bool {
154+
g.Bundles.ForEach(func(info bundle.ExecutionInfo) bool {
155155
bundleHistory[info.BlockNum] = append(bundleHistory[info.BlockNum], info)
156156
return true
157157
})

gossip/store_processed_bundles.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,14 @@ func (s *Store) GetProcessedBundlesHistory() (uint64, common.Hash, []bundle.Exec
210210
if len(res) != 16 {
211211
log.Crit("invalid data length for execution info", "length", len(res))
212212
}
213-
bNum := binary.BigEndian.Uint64(res[:8])
214-
startPosition := binary.BigEndian.Uint32(res[8:12])
215-
endPosition := binary.BigEndian.Uint32(res[12:])
213+
blockNumber := binary.BigEndian.Uint64(res[:8])
214+
position := binary.BigEndian.Uint32(res[8:12])
215+
count := binary.BigEndian.Uint32(res[12:])
216216
executedBundles = append(executedBundles, bundle.ExecutionInfo{
217217
ExecutionPlanHash: execPlanHash,
218-
BlockNum: bNum,
219-
Position: startPosition,
220-
Count: endPosition,
218+
BlockNum: blockNumber,
219+
Position: position,
220+
Count: count,
221221
})
222222
}
223223

gossip/store_processed_bundles_test.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,25 +170,28 @@ func TestStore_GetProcessedBundleHistory_ReturnsAllEntriesInStore(t *testing.T)
170170
hash1 := common.Hash{1, 2, 3}
171171
hash2 := common.Hash{4, 5, 6}
172172
hash3 := common.Hash{7, 8, 9}
173+
hash4 := common.Hash{10, 11, 12}
173174

175+
// Add single bundle
174176
store.AddProcessedBundles(1, []bundle.ExecutionInfo{wrapInfo(hash1)})
175-
176-
// blockNum1, hashAfterFirstAdd := store.GetProcessedBundleHistoryHash()
177-
// require.Equal(uint64(1), blockNum1)
178-
// require.NotZero(hashAfterFirstAdd)
179-
180-
store.AddProcessedBundles(2,
177+
// Add empty bundle list
178+
store.AddProcessedBundles(2, []bundle.ExecutionInfo{})
179+
// Add multiple bundles
180+
store.AddProcessedBundles(3,
181181
[]bundle.ExecutionInfo{
182182
wrapInfo(hash2),
183183
wrapInfo(hash3)})
184+
// Add bundle in a gapped block
185+
store.AddProcessedBundles(5, []bundle.ExecutionInfo{wrapInfo(hash4)})
184186

185187
lastBlockNumber, lastHistoryHash, allBundleInfos := store.GetProcessedBundlesHistory()
186-
require.Equal(uint64(2), lastBlockNumber)
188+
require.Equal(uint64(5), lastBlockNumber)
187189
require.NotZero(lastHistoryHash)
188-
require.Len(allBundleInfos, 3)
190+
require.Len(allBundleInfos, 4)
189191
require.Contains(allBundleInfos, wrapInfo(hash1))
190192
require.Contains(allBundleInfos, wrapInfo(hash2))
191193
require.Contains(allBundleInfos, wrapInfo(hash3))
194+
require.Contains(allBundleInfos, wrapInfo(hash4))
192195
}
193196

194197
func wrapInfo(hash common.Hash) bundle.ExecutionInfo {

opera/genesis/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ type (
7373
RawEvmItems EvmItems
7474
CommitteeCertificates SccCommitteeCertificates
7575
BlockCertificates SccBlockCertificates
76-
ProcessedBundles Bundles
76+
Bundles Bundles
7777
FwsLiveSection
7878
FwsArchiveSection
7979
SignatureSection

opera/genesisstore/store_genesis.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (s *Store) Genesis() genesis.Genesis {
7373
FwsLiveSection: s.FwsLiveSection(),
7474
FwsArchiveSection: s.FwsArchiveSection(),
7575
SignatureSection: s.SignatureSection(),
76-
ProcessedBundles: s.Bundles(),
76+
Bundles: s.Bundles(),
7777
}
7878
}
7979

@@ -226,11 +226,6 @@ func (s *Store) Bundles() genesis.Bundles {
226226
}
227227

228228
func (s RawBundles) ForEach(fn func(executionInfo bundle.ExecutionInfo) bool) {
229-
// TODO: implement de/serialize for bundle.ExecutionInfo
230-
231-
// Note: AddProcessedBundles is expected to be called once per block,
232-
// gather all bundles per block first and write them in a single call
233-
234229
for i := range int(bundle.MaxBlockRange) {
235230
f, err := s.fMap(BundlesSection(i))
236231
if err != nil {

tests/bundles/bundle_sponsored_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func TestBundle_CanRunSponsorshipAndSponsored(t *testing.T) {
9595
err = client.SendTransaction(t.Context(), envelope)
9696
require.NoError(t, err)
9797

98-
info, err := waitForBundleExecution(t.Context(), client.Client(), plan.Hash())
98+
info, err := WaitForBundleExecution(t.Context(), client.Client(), plan.Hash())
9999
require.NoError(t, err)
100100

101101
require.Equal(t, ethapi.BundleStatusExecuted, info.Status)

tests/bundles/bundle_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ func checkCase(t *testing.T, session tests.IntegrationTestNetSession, accounts *
562562
}
563563

564564
// Wait for the bundle to be processed.
565-
info, err := waitForBundleExecution(t.Context(), client.Client(), plan.Hash())
565+
info, err := WaitForBundleExecution(t.Context(), client.Client(), plan.Hash())
566566
require.NoError(t, err)
567567
require.NotNil(t, info.Block)
568568

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package bundles
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/0xsoniclabs/sonic/ethapi"
9+
"github.com/0xsoniclabs/sonic/tests"
10+
"github.com/ethereum/go-ethereum"
11+
"github.com/ethereum/go-ethereum/common"
12+
"github.com/ethereum/go-ethereum/common/hexutil"
13+
"github.com/ethereum/go-ethereum/core/types"
14+
"github.com/ethereum/go-ethereum/rpc"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
// Prepare bundle is a wrapper around the rpc method sonic_prepareBundle, which
19+
// prepares a bundle for execution by filling in all necessary fields and
20+
// encoding them properly.
21+
//
22+
// It accepts transactions in the form of CallMsg to keep compatibility with
23+
// standard go-ethereum client methods like EstimateGas.
24+
// CallMsg is a more convenient type to prepare transactions,
25+
// it does not encode fields into hex and is compatible with standard
26+
// go-ethereum client methods like EstimateGas.
27+
// Unfortunately, it does not include nonce, therefore this function needs
28+
// to assign a fitting value.
29+
//
30+
// This function should be part of the go-ethereum client object, being the entry
31+
// point to the api from go programs.
32+
func PrepareBundle(
33+
t *testing.T, client *tests.PooledEhtClient,
34+
earliest, latest uint64,
35+
txs []ethereum.CallMsg,
36+
) (ethapi.RPCPreparedBundle, error) {
37+
38+
nonces := make(map[common.Address]uint64)
39+
for _, tx := range txs {
40+
if _, ok := nonces[tx.From]; !ok {
41+
nonce, err := client.PendingNonceAt(t.Context(), tx.From)
42+
require.NoError(t, err, "failed to get pending nonce")
43+
nonces[tx.From] = nonce
44+
}
45+
}
46+
47+
// Convert CallMsg without nonce into TransactionArgs with nonce and hex encoding of fields
48+
txsArgs := make([]ethapi.TransactionArgs, len(txs))
49+
for i, tx := range txs {
50+
nonce := nonces[tx.From]
51+
nonces[tx.From] = nonce + 1
52+
txArgs := ethapi.TransactionArgs{
53+
From: &tx.From,
54+
To: tx.To,
55+
Nonce: (*hexutil.Uint64)(&nonce),
56+
GasPrice: (*hexutil.Big)(tx.GasPrice),
57+
Value: (*hexutil.Big)(tx.Value),
58+
Data: (*hexutil.Bytes)(&tx.Data),
59+
}
60+
txsArgs[i] = txArgs
61+
}
62+
63+
var gasLimits ethapi.BundleGasLimits
64+
err := client.Client().Call(&gasLimits, "sonic_estimateGasForTransactions", txsArgs, "latest", nil, nil)
65+
require.NoError(t, err, "failed to estimate gas for bundle")
66+
67+
for i := range txsArgs {
68+
txsArgs[i].Gas = (*hexutil.Uint64)(&gasLimits.GasLimits[i])
69+
}
70+
71+
// Call sonic_prepareBundle to get a bundle with all fields properly filled in and encoded
72+
var preparedBundle ethapi.RPCPreparedBundle
73+
err = client.Client().Call(&preparedBundle, "sonic_prepareBundle",
74+
ethapi.PrepareBundleArgs{
75+
Transactions: txsArgs,
76+
EarliestBlock: rpc.BlockNumber(earliest),
77+
LatestBlock: rpc.BlockNumber(latest),
78+
})
79+
require.NoError(t, err, "failed to call sonic_prepareBundle")
80+
return preparedBundle, nil
81+
}
82+
83+
// SubmitBundle is a wrapper around the rpc method sonic_submitBundle, which
84+
// submits a prepared bundle for execution.
85+
// It uses types.Transaction just like the method SendTransaction.
86+
// This function should be part of the go-ethereum client object, being the entry
87+
// point to the api from go programs.
88+
func SubmitBundle(client *tests.PooledEhtClient,
89+
txs []*types.Transaction,
90+
plan ethapi.RPCExecutionPlan,
91+
) (common.Hash, error) {
92+
encodedTransactions := make([]hexutil.Bytes, len(txs))
93+
for i, tx := range txs {
94+
data, err := tx.MarshalBinary()
95+
if err != nil {
96+
return common.Hash{}, fmt.Errorf("failed to marshal transaction: %w", err)
97+
}
98+
encodedTransactions[i] = hexutil.Bytes(data)
99+
}
100+
101+
var bundleHash common.Hash
102+
err := client.Client().Call(&bundleHash, "sonic_submitBundle",
103+
ethapi.SubmitBundleArgs{
104+
SignedTransactions: encodedTransactions,
105+
ExecutionPlan: plan,
106+
})
107+
return bundleHash, err
108+
}
109+
110+
func WaitForBundleExecution(
111+
ctxt context.Context,
112+
client *rpc.Client,
113+
executionPlanHash common.Hash,
114+
) (ethapi.RPCBundleInfo, error) {
115+
infos, err := waitForBundlesExecution(
116+
ctxt, client,
117+
[]common.Hash{executionPlanHash},
118+
)
119+
if err != nil {
120+
return ethapi.RPCBundleInfo{}, err
121+
}
122+
if len(infos) != 1 {
123+
return ethapi.RPCBundleInfo{}, fmt.Errorf("failed to obtain bundle info")
124+
}
125+
return infos[0], nil
126+
}
127+
128+
func getBundleInfo(
129+
ctxt context.Context,
130+
client *rpc.Client,
131+
executionPlanHash common.Hash,
132+
) (ethapi.RPCBundleInfo, error) {
133+
var info ethapi.RPCBundleInfo
134+
err := client.CallContext(
135+
ctxt,
136+
&info,
137+
"sonic_getBundleInfo",
138+
executionPlanHash,
139+
)
140+
return info, err
141+
}
142+
143+
func waitForBundlesExecution(
144+
ctxt context.Context,
145+
client *rpc.Client,
146+
executionPlanHashes []common.Hash,
147+
) ([]ethapi.RPCBundleInfo, error) {
148+
149+
infos := make([]ethapi.RPCBundleInfo, len(executionPlanHashes))
150+
done := make([]bool, len(executionPlanHashes))
151+
152+
err := tests.WaitFor(ctxt, func(innerCtx context.Context) (bool, error) {
153+
154+
allFinished := true
155+
for i, plan := range executionPlanHashes {
156+
if done[i] {
157+
continue
158+
}
159+
160+
info, err := getBundleInfo(innerCtx, client, plan)
161+
if err != nil {
162+
return false, err
163+
}
164+
165+
if info.Status != ethapi.BundleStatusPending {
166+
infos[i] = info
167+
done[i] = true
168+
} else {
169+
allFinished = false
170+
}
171+
}
172+
return allFinished, nil
173+
})
174+
return infos, err
175+
}

0 commit comments

Comments
 (0)