Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
N
nebula
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
exchain
nebula
Commits
fdfa5df3
Unverified
Commit
fdfa5df3
authored
Sep 06, 2023
by
Adrian Sutton
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Create an e2e test showing cannon proving an output root is invalid
parent
9e055c3b
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
228 additions
and
30 deletions
+228
-30
helper.go
op-e2e/e2eutils/disputegame/helper.go
+7
-30
helper.go
op-e2e/e2eutils/l2oo/helper.go
+130
-0
waits.go
op-e2e/e2eutils/wait/waits.go
+13
-0
faultproof_test.go
op-e2e/faultproof_test.go
+78
-0
No files found.
op-e2e/e2eutils/disputegame/helper.go
View file @
fdfa5df3
...
@@ -16,6 +16,7 @@ import (
...
@@ -16,6 +16,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/l2oo"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup"
...
@@ -65,7 +66,7 @@ type FactoryHelper struct {
...
@@ -65,7 +66,7 @@ type FactoryHelper struct {
factoryAddr
common
.
Address
factoryAddr
common
.
Address
factory
*
bindings
.
DisputeGameFactory
factory
*
bindings
.
DisputeGameFactory
blockOracle
*
bindings
.
BlockOracle
blockOracle
*
bindings
.
BlockOracle
l2oo
*
bindings
.
L2OutputOracleCall
er
l2oo
Helper
*
l2oo
.
L2OOHelp
er
}
}
func
NewFactoryHelper
(
t
*
testing
.
T
,
ctx
context
.
Context
,
deployments
*
genesis
.
L1Deployments
,
client
*
ethclient
.
Client
)
*
FactoryHelper
{
func
NewFactoryHelper
(
t
*
testing
.
T
,
ctx
context
.
Context
,
deployments
*
genesis
.
L1Deployments
,
client
*
ethclient
.
Client
)
*
FactoryHelper
{
...
@@ -81,8 +82,6 @@ func NewFactoryHelper(t *testing.T, ctx context.Context, deployments *genesis.L1
...
@@ -81,8 +82,6 @@ func NewFactoryHelper(t *testing.T, ctx context.Context, deployments *genesis.L1
require
.
NoError
(
err
)
require
.
NoError
(
err
)
blockOracle
,
err
:=
bindings
.
NewBlockOracle
(
deployments
.
BlockOracle
,
client
)
blockOracle
,
err
:=
bindings
.
NewBlockOracle
(
deployments
.
BlockOracle
,
client
)
require
.
NoError
(
err
)
require
.
NoError
(
err
)
l2oo
,
err
:=
bindings
.
NewL2OutputOracleCaller
(
deployments
.
L2OutputOracleProxy
,
client
)
require
.
NoError
(
err
,
"Error creating l2oo caller"
)
return
&
FactoryHelper
{
return
&
FactoryHelper
{
t
:
t
,
t
:
t
,
...
@@ -92,7 +91,7 @@ func NewFactoryHelper(t *testing.T, ctx context.Context, deployments *genesis.L1
...
@@ -92,7 +91,7 @@ func NewFactoryHelper(t *testing.T, ctx context.Context, deployments *genesis.L1
factory
:
factory
,
factory
:
factory
,
factoryAddr
:
factoryAddr
,
factoryAddr
:
factoryAddr
,
blockOracle
:
blockOracle
,
blockOracle
:
blockOracle
,
l2oo
:
l2oo
,
l2oo
Helper
:
l2oo
.
NewL2OOHelperReadOnly
(
t
,
deployments
,
client
)
,
}
}
}
}
...
@@ -150,12 +149,8 @@ func (h *FactoryHelper) StartCannonGameWithCorrectRoot(ctx context.Context, roll
...
@@ -150,12 +149,8 @@ func (h *FactoryHelper) StartCannonGameWithCorrectRoot(ctx context.Context, roll
challengerOpts
=
append
(
challengerOpts
,
options
...
)
challengerOpts
=
append
(
challengerOpts
,
options
...
)
cfg
:=
challenger
.
NewChallengerConfig
(
h
.
t
,
l1Endpoint
,
challengerOpts
...
)
cfg
:=
challenger
.
NewChallengerConfig
(
h
.
t
,
l1Endpoint
,
challengerOpts
...
)
opts
:=
&
bind
.
CallOpts
{
Context
:
ctx
}
opts
:=
&
bind
.
CallOpts
{
Context
:
ctx
}
outputIdx
,
err
:=
h
.
l2oo
.
GetL2OutputIndexAfter
(
opts
,
new
(
big
.
Int
)
.
SetUint64
(
l2BlockNumber
))
challengedOutput
:=
h
.
l2ooHelper
.
GetL2OutputAfter
(
ctx
,
l2BlockNumber
)
h
.
require
.
NoError
(
err
,
"Fetch challenged output index"
)
agreedOutput
:=
h
.
l2ooHelper
.
GetL2OutputBefore
(
ctx
,
l2BlockNumber
)
challengedOutput
,
err
:=
h
.
l2oo
.
GetL2Output
(
opts
,
outputIdx
)
h
.
require
.
NoError
(
err
,
"Fetch challenged output"
)
agreedOutput
,
err
:=
h
.
l2oo
.
GetL2Output
(
opts
,
new
(
big
.
Int
)
.
Sub
(
outputIdx
,
common
.
Big1
))
h
.
require
.
NoError
(
err
,
"Fetch agreed output"
)
l1BlockInfo
,
err
:=
h
.
blockOracle
.
Load
(
opts
,
l1Head
)
l1BlockInfo
,
err
:=
h
.
blockOracle
.
Load
(
opts
,
l1Head
)
h
.
require
.
NoError
(
err
,
"Fetch L1 block info"
)
h
.
require
.
NoError
(
err
,
"Fetch L1 block info"
)
...
@@ -246,26 +241,8 @@ func (h *FactoryHelper) prepareCannonGame(ctx context.Context) (uint64, *big.Int
...
@@ -246,26 +241,8 @@ func (h *FactoryHelper) prepareCannonGame(ctx context.Context) (uint64, *big.Int
func
(
h
*
FactoryHelper
)
waitForProposals
(
ctx
context
.
Context
)
uint64
{
func
(
h
*
FactoryHelper
)
waitForProposals
(
ctx
context
.
Context
)
uint64
{
ctx
,
cancel
:=
context
.
WithTimeout
(
ctx
,
2
*
time
.
Minute
)
ctx
,
cancel
:=
context
.
WithTimeout
(
ctx
,
2
*
time
.
Minute
)
defer
cancel
()
defer
cancel
()
opts
:=
&
bind
.
CallOpts
{
Context
:
ctx
}
latestOutputIdx
:=
h
.
l2ooHelper
.
WaitForProposals
(
ctx
,
2
)
latestOutputIndex
,
err
:=
wait
.
AndGet
(
return
h
.
l2ooHelper
.
GetL2Output
(
ctx
,
latestOutputIdx
)
.
L2BlockNumber
.
Uint64
()
ctx
,
time
.
Second
,
func
()
(
*
big
.
Int
,
error
)
{
index
,
err
:=
h
.
l2oo
.
LatestOutputIndex
(
opts
)
if
err
!=
nil
{
h
.
t
.
Logf
(
"Could not get latest output index: %v"
,
err
.
Error
())
return
nil
,
nil
}
h
.
t
.
Logf
(
"Latest output index: %v"
,
index
)
return
index
,
nil
},
func
(
index
*
big
.
Int
)
bool
{
return
index
!=
nil
&&
index
.
Cmp
(
big
.
NewInt
(
1
))
>=
0
})
h
.
require
.
NoError
(
err
,
"Did not get two output roots"
)
output
,
err
:=
h
.
l2oo
.
GetL2Output
(
opts
,
latestOutputIndex
)
h
.
require
.
NoErrorf
(
err
,
"Could not get latst output root index: %v"
,
latestOutputIndex
)
return
output
.
L2BlockNumber
.
Uint64
()
}
}
// checkpointL1Block stores the current L1 block in the oracle
// checkpointL1Block stores the current L1 block in the oracle
...
...
op-e2e/e2eutils/l2oo/helper.go
0 → 100644
View file @
fdfa5df3
package
l2oo
import
(
"context"
"crypto/ecdsa"
"math/big"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
)
type
L2OOHelper
struct
{
t
*
testing
.
T
require
*
require
.
Assertions
client
*
ethclient
.
Client
l2oo
*
bindings
.
L2OutputOracle
// Nil when read-only
transactOpts
*
bind
.
TransactOpts
rollupCfg
*
rollup
.
Config
}
func
NewL2OOHelperReadOnly
(
t
*
testing
.
T
,
deployments
*
genesis
.
L1Deployments
,
client
*
ethclient
.
Client
)
*
L2OOHelper
{
require
:=
require
.
New
(
t
)
l2oo
,
err
:=
bindings
.
NewL2OutputOracle
(
deployments
.
L2OutputOracleProxy
,
client
)
require
.
NoError
(
err
,
"Error creating l2oo bindings"
)
return
&
L2OOHelper
{
t
:
t
,
require
:
require
,
client
:
client
,
l2oo
:
l2oo
,
}
}
func
NewL2OOHelper
(
t
*
testing
.
T
,
deployments
*
genesis
.
L1Deployments
,
client
*
ethclient
.
Client
,
proposerKey
*
ecdsa
.
PrivateKey
,
rollupCfg
*
rollup
.
Config
)
*
L2OOHelper
{
h
:=
NewL2OOHelperReadOnly
(
t
,
deployments
,
client
)
chainID
,
err
:=
client
.
ChainID
(
context
.
Background
())
h
.
require
.
NoError
(
err
,
"Failed to get chain ID"
)
transactOpts
,
err
:=
bind
.
NewKeyedTransactorWithChainID
(
proposerKey
,
chainID
)
h
.
require
.
NoError
(
err
)
h
.
transactOpts
=
transactOpts
h
.
rollupCfg
=
rollupCfg
return
h
}
// WaitForProposals waits until there are at least the specified number of proposals in the output oracle
// Returns the index of the latest output proposal
func
(
h
*
L2OOHelper
)
WaitForProposals
(
ctx
context
.
Context
,
req
int64
)
uint64
{
ctx
,
cancel
:=
context
.
WithTimeout
(
ctx
,
2
*
time
.
Minute
)
defer
cancel
()
opts
:=
&
bind
.
CallOpts
{
Context
:
ctx
}
latestOutputIndex
,
err
:=
wait
.
AndGet
(
ctx
,
time
.
Second
,
func
()
(
*
big
.
Int
,
error
)
{
index
,
err
:=
h
.
l2oo
.
LatestOutputIndex
(
opts
)
if
err
!=
nil
{
h
.
t
.
Logf
(
"Could not get latest output index: %v"
,
err
.
Error
())
return
nil
,
nil
}
h
.
t
.
Logf
(
"Latest output index: %v"
,
index
)
return
index
,
nil
},
func
(
index
*
big
.
Int
)
bool
{
return
index
!=
nil
&&
index
.
Cmp
(
big
.
NewInt
(
req
-
1
))
>=
0
})
h
.
require
.
NoErrorf
(
err
,
"Did not get %v output roots"
,
req
)
return
latestOutputIndex
.
Uint64
()
}
func
(
h
*
L2OOHelper
)
GetL2Output
(
ctx
context
.
Context
,
idx
uint64
)
bindings
.
TypesOutputProposal
{
output
,
err
:=
h
.
l2oo
.
GetL2Output
(
&
bind
.
CallOpts
{
Context
:
ctx
},
new
(
big
.
Int
)
.
SetUint64
(
idx
))
h
.
require
.
NoErrorf
(
err
,
"Failed to get output root at index: %v"
,
idx
)
return
output
}
func
(
h
*
L2OOHelper
)
GetL2OutputAfter
(
ctx
context
.
Context
,
l2BlockNum
uint64
)
bindings
.
TypesOutputProposal
{
opts
:=
&
bind
.
CallOpts
{
Context
:
ctx
}
outputIdx
,
err
:=
h
.
l2oo
.
GetL2OutputIndexAfter
(
opts
,
new
(
big
.
Int
)
.
SetUint64
(
l2BlockNum
))
h
.
require
.
NoError
(
err
,
"Fetch challenged output index"
)
output
,
err
:=
h
.
l2oo
.
GetL2Output
(
opts
,
outputIdx
)
h
.
require
.
NoError
(
err
,
"Fetch challenged output"
)
return
output
}
func
(
h
*
L2OOHelper
)
GetL2OutputBefore
(
ctx
context
.
Context
,
l2BlockNum
uint64
)
bindings
.
TypesOutputProposal
{
opts
:=
&
bind
.
CallOpts
{
Context
:
ctx
}
latestBlockNum
,
err
:=
h
.
l2oo
.
LatestBlockNumber
(
opts
)
h
.
require
.
NoError
(
err
,
"Failed to get latest output root block number"
)
var
outputIdx
*
big
.
Int
if
latestBlockNum
.
Uint64
()
<
l2BlockNum
{
outputIdx
,
err
=
h
.
l2oo
.
LatestOutputIndex
(
opts
)
h
.
require
.
NoError
(
err
,
"Failed to get latest output index"
)
}
else
{
outputIdx
,
err
=
h
.
l2oo
.
GetL2OutputIndexAfter
(
opts
,
new
(
big
.
Int
)
.
SetUint64
(
l2BlockNum
))
h
.
require
.
NoErrorf
(
err
,
"Failed to get output index after block %v"
,
l2BlockNum
)
h
.
require
.
NotZerof
(
outputIdx
.
Uint64
(),
"No l2 output before block %v"
,
l2BlockNum
)
outputIdx
=
new
(
big
.
Int
)
.
Sub
(
outputIdx
,
common
.
Big1
)
}
return
h
.
GetL2Output
(
ctx
,
outputIdx
.
Uint64
())
}
func
(
h
*
L2OOHelper
)
PublishNextOutput
(
ctx
context
.
Context
,
outputRoot
common
.
Hash
)
{
h
.
require
.
NotNil
(
h
.
transactOpts
,
"Can't publish outputs from a read only L2OOHelper"
)
nextBlockNum
,
err
:=
h
.
l2oo
.
NextBlockNumber
(
&
bind
.
CallOpts
{
Context
:
ctx
})
h
.
require
.
NoError
(
err
,
"Should get next block number"
)
genesis
:=
h
.
rollupCfg
.
Genesis
targetTimestamp
:=
genesis
.
L2Time
+
((
nextBlockNum
.
Uint64
()
-
genesis
.
L2
.
Number
)
*
h
.
rollupCfg
.
BlockTime
)
timedCtx
,
cancel
:=
context
.
WithTimeout
(
ctx
,
30
*
time
.
Second
)
defer
cancel
()
h
.
require
.
NoErrorf
(
wait
.
ForBlockWithTimestamp
(
timedCtx
,
h
.
client
,
targetTimestamp
),
"Wait for L1 block with timestamp >= %v"
,
targetTimestamp
)
tx
,
err
:=
h
.
l2oo
.
ProposeL2Output
(
h
.
transactOpts
,
outputRoot
,
nextBlockNum
,
[
32
]
byte
{},
common
.
Big0
)
h
.
require
.
NoErrorf
(
err
,
"Failed to propose output root for l2 block number %v"
,
nextBlockNum
)
_
,
err
=
wait
.
ForReceiptOK
(
ctx
,
h
.
client
,
tx
.
Hash
())
h
.
require
.
NoErrorf
(
err
,
"Proposal for l2 block %v failed"
,
nextBlockNum
)
}
op-e2e/e2eutils/wait/waits.go
View file @
fdfa5df3
...
@@ -85,6 +85,19 @@ func ForBlock(ctx context.Context, client *ethclient.Client, n uint64) error {
...
@@ -85,6 +85,19 @@ func ForBlock(ctx context.Context, client *ethclient.Client, n uint64) error {
return
nil
return
nil
}
}
func
ForBlockWithTimestamp
(
ctx
context
.
Context
,
client
*
ethclient
.
Client
,
target
uint64
)
error
{
_
,
err
:=
AndGet
(
ctx
,
time
.
Second
,
func
()
(
uint64
,
error
)
{
head
,
err
:=
client
.
BlockByNumber
(
ctx
,
nil
)
if
err
!=
nil
{
return
0
,
err
}
return
head
.
Time
(),
nil
},
func
(
actual
uint64
)
bool
{
return
actual
>=
target
})
return
err
}
func
ForNextBlock
(
ctx
context
.
Context
,
client
*
ethclient
.
Client
)
error
{
func
ForNextBlock
(
ctx
context
.
Context
,
client
*
ethclient
.
Client
)
error
{
current
,
err
:=
client
.
BlockNumber
(
ctx
)
current
,
err
:=
client
.
BlockNumber
(
ctx
)
if
err
!=
nil
{
if
err
!=
nil
{
...
...
op-e2e/faultproof_test.go
View file @
fdfa5df3
...
@@ -6,6 +6,7 @@ import (
...
@@ -6,6 +6,7 @@ import (
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame"
l2oo2
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/l2oo"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/ethclient"
...
@@ -355,6 +356,83 @@ func TestCannonDefendStep(t *testing.T) {
...
@@ -355,6 +356,83 @@ func TestCannonDefendStep(t *testing.T) {
game
.
LogGameData
(
ctx
)
game
.
LogGameData
(
ctx
)
}
}
func
TestCannonProposedOutputRootInvalid
(
t
*
testing
.
T
)
{
InitParallel
(
t
)
ctx
:=
context
.
Background
()
sys
,
l1Client
,
game
,
correctTrace
:=
setupDisputeGameForInvalidOutputRoot
(
t
,
common
.
Hash
{
0xab
})
t
.
Cleanup
(
sys
.
Close
)
maxDepth
:=
game
.
MaxDepth
(
ctx
)
// Now maliciously play the game and it should be impossible to win
for
claimCount
:=
int64
(
1
);
claimCount
<
maxDepth
;
{
// Attack everything but oddly using the correct hash.
correctTrace
.
Attack
(
ctx
,
claimCount
-
1
)
claimCount
++
game
.
LogGameData
(
ctx
)
game
.
WaitForClaimCount
(
ctx
,
claimCount
)
game
.
LogGameData
(
ctx
)
// Wait for the challenger to counter
claimCount
++
game
.
WaitForClaimCount
(
ctx
,
claimCount
)
}
game
.
LogGameData
(
ctx
)
// Wait for the challenger to call step and counter our invalid claim
game
.
WaitForClaimAtMaxDepth
(
ctx
,
false
)
// It's on us to call step if we want to win but shouldn't be possible
// Need to add support for this to the helper
// Time travel past when the game will be resolvable.
sys
.
TimeTravelClock
.
AdvanceTime
(
game
.
GameDuration
(
ctx
))
require
.
NoError
(
t
,
wait
.
ForNextBlock
(
ctx
,
l1Client
))
game
.
WaitForGameStatus
(
ctx
,
disputegame
.
StatusDefenderWins
)
game
.
LogGameData
(
ctx
)
}
// setupDisputeGameForInvalidOutputRoot sets up an L2 chain with at least one valid output root followed by an invalid output root.
// A cannon dispute game is started to dispute the invalid output root with the correct root claim provided.
// An honest challenger is run to defend the root claim (ie disagree with the invalid output root).
func
setupDisputeGameForInvalidOutputRoot
(
t
*
testing
.
T
,
outputRoot
common
.
Hash
)
(
*
System
,
*
ethclient
.
Client
,
*
disputegame
.
CannonGameHelper
,
*
disputegame
.
HonestHelper
)
{
ctx
:=
context
.
Background
()
sys
,
l1Client
:=
startFaultDisputeSystem
(
t
)
l2oo
:=
l2oo2
.
NewL2OOHelper
(
t
,
sys
.
cfg
.
L1Deployments
,
l1Client
,
sys
.
cfg
.
Secrets
.
Proposer
,
sys
.
RollupConfig
)
// Wait for one valid output root to be submitted
l2oo
.
WaitForProposals
(
ctx
,
1
)
// Stop the honest output submitter so we can publish invalid outputs
sys
.
L2OutputSubmitter
.
Stop
()
sys
.
L2OutputSubmitter
=
nil
// Submit an invalid output rooot
l2oo
.
PublishNextOutput
(
ctx
,
outputRoot
)
l1Endpoint
:=
sys
.
NodeEndpoint
(
"l1"
)
l2Endpoint
:=
sys
.
NodeEndpoint
(
"sequencer"
)
// Dispute the new output root by creating a new game with the correct cannon trace.
disputeGameFactory
:=
disputegame
.
NewFactoryHelper
(
t
,
ctx
,
sys
.
cfg
.
L1Deployments
,
l1Client
)
game
,
correctTrace
:=
disputeGameFactory
.
StartCannonGameWithCorrectRoot
(
ctx
,
sys
.
RollupConfig
,
sys
.
L2GenesisCfg
,
l1Endpoint
,
l2Endpoint
,
challenger
.
WithPrivKey
(
sys
.
cfg
.
Secrets
.
Mallory
),
)
require
.
NotNil
(
t
,
game
)
// Start the honest challenger
game
.
StartChallenger
(
ctx
,
sys
.
RollupConfig
,
sys
.
L2GenesisCfg
,
l1Endpoint
,
l2Endpoint
,
"Defender"
,
// Disagree with the proposed output, so agree with the (correct) root claim
challenger
.
WithAgreeProposedOutput
(
false
),
challenger
.
WithPrivKey
(
sys
.
cfg
.
Secrets
.
Mallory
),
)
return
sys
,
l1Client
,
game
,
correctTrace
}
func
TestCannonChallengeWithCorrectRoot
(
t
*
testing
.
T
)
{
func
TestCannonChallengeWithCorrectRoot
(
t
*
testing
.
T
)
{
t
.
Skip
(
"Not currently handling this case as the correct approach will change when output root bisection is added"
)
t
.
Skip
(
"Not currently handling this case as the correct approach will change when output root bisection is added"
)
InitParallel
(
t
)
InitParallel
(
t
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment