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
ac5b061d
Unverified
Commit
ac5b061d
authored
Mar 22, 2024
by
smartcontracts
Committed by
GitHub
Mar 22, 2024
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(sdk): update SDK to support multiple withdrawal proofs (#9951)
parent
90147ac1
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
209 additions
and
94 deletions
+209
-94
slimy-ducks-promise.md
.changeset/slimy-ducks-promise.md
+5
-0
cross-chain-messenger.ts
packages/sdk/src/cross-chain-messenger.ts
+188
-92
types.ts
packages/sdk/src/interfaces/types.ts
+16
-2
No files found.
.changeset/slimy-ducks-promise.md
0 → 100644
View file @
ac5b061d
---
'
@eth-optimism/sdk'
:
minor
---
Updates SDK for FPAC proven withdrawals mapping.
packages/sdk/src/cross-chain-messenger.ts
View file @
ac5b061d
...
@@ -57,6 +57,7 @@ import {
...
@@ -57,6 +57,7 @@ import {
IBridgeAdapter
,
IBridgeAdapter
,
ProvenWithdrawal
,
ProvenWithdrawal
,
LowLevelMessage
,
LowLevelMessage
,
FPACProvenWithdrawal
,
}
from
'
./interfaces
'
}
from
'
./interfaces
'
import
{
import
{
toSignerOrProvider
,
toSignerOrProvider
,
...
@@ -762,19 +763,16 @@ export class CrossChainMessenger {
...
@@ -762,19 +763,16 @@ export class CrossChainMessenger {
messageIndex
messageIndex
)
)
// Pick portal based on FPAC compatibility.
const
portal
=
(
await
this
.
fpac
())
?
this
.
contracts
.
l1
.
OptimismPortal2
:
this
.
contracts
.
l1
.
OptimismPortal
// Attempt to fetch the proven withdrawal.
// Attempt to fetch the proven withdrawal.
const
provenWithdrawal
=
await
portal
.
provenWithdrawals
(
const
provenWithdrawal
=
await
this
.
getProvenWithdrawal
(
hashLowLevelMessage
(
withdrawal
)
hashLowLevelMessage
(
withdrawal
)
)
)
// If the withdrawal hash has not been proven on L1,
// If the withdrawal hash has not been proven on L1, return READY_TO_PROVE.
// return `READY_TO_PROVE`
// Note that this will also apply in the case that a withdrawal has been proven but the
if
(
provenWithdrawal
.
timestamp
.
eq
(
BigNumber
.
from
(
0
)))
{
// proposal used to create the proof was invalidated. This is fine because in that case
// the withdrawal needs to be proven again anyway.
if
(
provenWithdrawal
===
null
)
{
return
MessageStatus
.
READY_TO_PROVE
return
MessageStatus
.
READY_TO_PROVE
}
}
...
@@ -805,32 +803,32 @@ export class CrossChainMessenger {
...
@@ -805,32 +803,32 @@ export class CrossChainMessenger {
const
withdrawalHash
=
hashLowLevelMessage
(
withdrawal
)
const
withdrawalHash
=
hashLowLevelMessage
(
withdrawal
)
// Grab the proven withdrawal data.
// Grab the proven withdrawal data.
const
provenWithdrawal
=
const
provenWithdrawal
=
await
this
.
getProvenWithdrawal
(
await
this
.
contracts
.
l1
.
OptimismPortal2
.
provenWithdrawals
(
withdrawalHash
withdrawalHash
)
)
// Attach to the FaultDisputeGame.
// Sanity check, should've already happened above but do it just in case.
const
game
=
new
ethers
.
Contract
(
if
(
provenWithdrawal
===
null
)
{
provenWithdrawal
.
disputeGameProxy
,
// Ready to prove is the correct status here, we would not expect to hit this code path
getContractInterfaceBedrock
(
'
FaultDisputeGame
'
),
// unless there was an unexpected reorg on L1. Since this is unlikely we log a warning.
this
.
l1SignerOrProvider
console
.
warn
(
'
Unexpected code path reached in getMessageStatus, returning READY_TO_PROVE
'
)
)
return
MessageStatus
.
READY_TO_PROVE
}
// Check if the game resolved to status 1 = "CHALLENGER_WINS". If so, the withdrawal was
// Shouldn't happen, but worth checking just in case.
// proven against a proposal that was invalidated and will need to be reproven. We throw
if
(
!
(
'
proofSubmitter
'
in
provenWithdrawal
))
{
// an error here instead of creating a new status mostly because it's easier to integrate
throw
new
Error
(
// into the SDK.
`expected to get FPAC withdrawal but got legacy withdrawal`
const
status
=
await
game
.
status
()
)
if
(
status
===
1
)
{
throw
new
Error
(
`withdrawal proposal was invalidated, must reprove`
)
}
}
try
{
try
{
// If this doesn't revert then we should be fine to relay.
// If this doesn't revert then we should be fine to relay.
await
this
.
contracts
.
l1
.
OptimismPortal2
.
checkWithdrawal
(
await
this
.
contracts
.
l1
.
OptimismPortal2
.
checkWithdrawal
(
hashLowLevelMessage
(
withdrawal
),
hashLowLevelMessage
(
withdrawal
),
await
this
.
l1Signer
.
getAddress
()
provenWithdrawal
.
proofSubmitter
)
)
return
MessageStatus
.
READY_FOR_RELAY
return
MessageStatus
.
READY_FOR_RELAY
...
@@ -1267,12 +1265,168 @@ export class CrossChainMessenger {
...
@@ -1267,12 +1265,168 @@ export class CrossChainMessenger {
*/
*/
public
async
getProvenWithdrawal
(
public
async
getProvenWithdrawal
(
withdrawalHash
:
string
withdrawalHash
:
string
):
Promise
<
ProvenWithdrawal
>
{
):
Promise
<
ProvenWithdrawal
|
null
>
{
if
(
!
this
.
bedrock
)
{
if
(
!
this
.
bedrock
)
{
throw
new
Error
(
'
message proving only applies after the bedrock upgrade
'
)
throw
new
Error
(
'
message proving only applies after the bedrock upgrade
'
)
}
}
return
this
.
contracts
.
l1
.
OptimismPortal
.
provenWithdrawals
(
withdrawalHash
)
// Getting the withdrawal is easy before FPAC.
if
(
!
(
await
this
.
fpac
()))
{
// Grab the proven withdrawal directly by hash.
const
provenWithdrawal
=
await
this
.
contracts
.
l1
.
OptimismPortal
.
provenWithdrawals
(
withdrawalHash
)
// If the timestamp is 0 then the withdrawal has not been proven.
if
(
provenWithdrawal
.
timestamp
.
eq
(
0
))
{
return
null
}
else
{
return
provenWithdrawal
}
}
// Getting the withdrawal is a bit more complicated after FPAC.
// First we need to get the number of proof submitters for this withdrawal.
const
numProofSubmitters
=
BigNumber
.
from
(
await
this
.
contracts
.
l1
.
OptimismPortal2
.
numProofSubmitters
(
withdrawalHash
)
).
toNumber
()
// Now we need to find any withdrawal where the output proposal that the withdrawal was proven
// against is actually valid. We can use the same output validation cache used elsewhere.
for
(
let
i
=
0
;
i
<
numProofSubmitters
;
i
++
)
{
// Grab the proof submitter.
const
proofSubmitter
=
await
this
.
contracts
.
l1
.
OptimismPortal2
.
proofSubmitters
(
withdrawalHash
,
i
)
// Grab the ProvenWithdrawal struct for this proof.
const
provenWithdrawal
=
await
this
.
contracts
.
l1
.
OptimismPortal2
.
provenWithdrawals
(
withdrawalHash
,
proofSubmitter
)
// Grab the game that was proven against.
const
game
=
new
ethers
.
Contract
(
provenWithdrawal
.
disputeGameProxy
,
getContractInterfaceBedrock
(
'
FaultDisputeGame
'
),
this
.
l1SignerOrProvider
)
// Check the game status.
const
status
=
await
game
.
status
()
if
(
status
===
1
)
{
// If status is CHALLENGER_WINS then it's no good.
continue
}
else
if
(
status
===
2
)
{
// If status is DEFENDER_WINS then it's a valid proof.
return
{
...
provenWithdrawal
,
proofSubmitter
,
}
}
else
if
(
status
>
2
)
{
// Shouldn't happen in practice.
throw
new
Error
(
'
got invalid game status
'
)
}
// Otherwise we're IN_PROGRESS.
// Grab the block number from the extra data. Since this is not a standardized field we need
// to be defensive and assume that the extra data could be anything. If the extra data does
// not decode properly then we just skip this game.
const
extraData
=
await
game
.
extraData
()
let
l2BlockNumber
:
number
try
{
;[
l2BlockNumber
]
=
ethers
.
utils
.
defaultAbiCoder
.
decode
(
[
'
uint256
'
],
extraData
)
}
catch
(
err
)
{
// Didn't decode properly, bad game.
continue
}
// Finally we check if the output root is valid. If it is, then we can return the proven
// withdrawal. If it isn't, then we act as if this proof does not exist because it isn't
// useful for finalizing the withdrawal.
if
(
await
this
.
isValidOutputRoot
(
await
game
.
rootClaim
(),
l2BlockNumber
))
{
return
{
...
provenWithdrawal
,
proofSubmitter
,
}
}
}
// Return null if we didn't find a valid proof.
return
null
}
/**
* Checks whether a given root claim is valid. Uses the L2 node that the SDK is connected to
* when verifying the claim. Assumes that the connected L2 node is honest.
*
* @param outputRoot Output root to verify.
* @param l2BlockNumber L2 block number the root is for.
* @returns Whether or not the root is valid.
*/
public
async
isValidOutputRoot
(
outputRoot
:
string
,
l2BlockNumber
:
number
):
Promise
<
boolean
>
{
// Use the cache if we can.
const
cached
=
this
.
_outputCache
.
find
((
other
)
=>
{
return
other
.
root
===
outputRoot
})
// Skip if we can use the cached.
if
(
cached
)
{
return
cached
.
valid
}
// If the cache ever gets to 10k elements, clear out the first half. Works well enough
// since the cache will generally tend to be used in a FIFO manner.
if
(
this
.
_outputCache
.
length
>
10000
)
{
this
.
_outputCache
=
this
.
_outputCache
.
slice
(
5000
)
}
// We didn't hit the cache so we're going to have to do the work.
try
{
// Make sure this is a JSON RPC provider.
const
provider
=
toJsonRpcProvider
(
this
.
l2Provider
)
// Grab the block and storage proof at the same time.
const
[
block
,
proof
]
=
await
Promise
.
all
([
provider
.
send
(
'
eth_getBlockByNumber
'
,
[
toRpcHexString
(
l2BlockNumber
),
false
,
]),
makeStateTrieProof
(
provider
,
l2BlockNumber
,
this
.
contracts
.
l2
.
OVM_L2ToL1MessagePasser
.
address
,
ethers
.
constants
.
HashZero
),
])
// Compute the output.
const
output
=
ethers
.
utils
.
solidityKeccak256
(
[
'
bytes32
'
,
'
bytes32
'
,
'
bytes32
'
,
'
bytes32
'
],
[
ethers
.
constants
.
HashZero
,
block
.
stateRoot
,
proof
.
storageRoot
,
block
.
hash
,
]
)
// If the output matches the proposal then we're good.
const
valid
=
output
===
outputRoot
this
.
_outputCache
.
push
({
root
:
outputRoot
,
valid
})
return
valid
}
catch
(
err
)
{
// Assume the game is invalid but don't add it to the cache just in case we had a temp error.
return
false
}
}
}
/**
/**
...
@@ -1342,69 +1496,11 @@ export class CrossChainMessenger {
...
@@ -1342,69 +1496,11 @@ export class CrossChainMessenger {
// Now we verify the proposals in the matches array.
// Now we verify the proposals in the matches array.
let
match
:
any
let
match
:
any
for
(
const
option
of
matches
)
{
for
(
const
option
of
matches
)
{
// Use the cache if we can.
if
(
const
cached
=
this
.
_outputCache
.
find
((
other
)
=>
{
await
this
.
isValidOutputRoot
(
option
.
rootClaim
,
option
.
l2BlockNumber
)
return
other
.
root
===
option
.
rootClaim
)
{
})
// Skip if we can use the cached.
if
(
cached
)
{
if
(
cached
.
valid
)
{
match
=
option
break
}
else
{
continue
}
}
// If the cache ever gets to 10k elements, clear out the first half. Works well enough
// since the cache will generally tend to be used in a FIFO manner.
if
(
this
.
_outputCache
.
length
>
10000
)
{
this
.
_outputCache
=
this
.
_outputCache
.
slice
(
5000
)
}
// We didn't hit the cache so we're going to have to do the work.
try
{
// Make sure this is a JSON RPC provider.
const
provider
=
toJsonRpcProvider
(
this
.
l2Provider
)
// Grab the block and storage proof at the same time.
const
[
block
,
proof
]
=
await
Promise
.
all
([
provider
.
send
(
'
eth_getBlockByNumber
'
,
[
toRpcHexString
(
option
.
l2BlockNumber
),
false
,
]),
makeStateTrieProof
(
provider
,
option
.
l2BlockNumber
,
this
.
contracts
.
l2
.
OVM_L2ToL1MessagePasser
.
address
,
ethers
.
constants
.
HashZero
),
])
// Compute the output.
const
output
=
ethers
.
utils
.
solidityKeccak256
(
[
'
bytes32
'
,
'
bytes32
'
,
'
bytes32
'
,
'
bytes32
'
],
[
ethers
.
constants
.
HashZero
,
block
.
stateRoot
,
proof
.
storageRoot
,
block
.
hash
,
]
)
// If the output matches the proposal then we're good.
if
(
output
===
option
.
rootClaim
)
{
this
.
_outputCache
.
push
({
root
:
option
.
rootClaim
,
valid
:
true
})
match
=
option
match
=
option
break
break
}
else
{
this
.
_outputCache
.
push
({
root
:
option
.
rootClaim
,
valid
:
false
})
}
}
catch
(
err
)
{
// Just skip this option, whatever. If it was a transient error then we'll try again in
// the next loop iteration. If it was a permanent error then we'll get the same thing.
continue
}
}
}
}
...
...
packages/sdk/src/interfaces/types.ts
View file @
ac5b061d
...
@@ -258,14 +258,28 @@ export interface MessageReceipt {
...
@@ -258,14 +258,28 @@ export interface MessageReceipt {
}
}
/**
/**
* ProvenWithdrawal in OptimismPortal
* ProvenWithdrawal in OptimismPortal
.
*/
*/
export
interface
ProvenWithdrawal
{
export
interface
Legacy
ProvenWithdrawal
{
outputRoot
:
string
outputRoot
:
string
timestamp
:
BigNumber
timestamp
:
BigNumber
l2BlockNumber
:
BigNumber
l2BlockNumber
:
BigNumber
}
}
/**
* ProvenWithdrawal in OptimismPortal (FPAC).
*/
export
interface
FPACProvenWithdrawal
{
proofSubmitter
:
string
disputeGameProxy
:
string
timestamp
:
BigNumber
}
/**
* ProvenWithdrawal in OptimismPortal (FPAC or Legacy).
*/
export
type
ProvenWithdrawal
=
LegacyProvenWithdrawal
|
FPACProvenWithdrawal
/**
/**
* Header for a state root batch.
* Header for a state root batch.
*/
*/
...
...
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