Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Assertion Auto Pooling #648

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
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
28 changes: 28 additions & 0 deletions assertions/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ var (
safeBlockDelayCounter = metrics.GetOrRegisterCounter("arb/validator/scanner/safe_block_delay", nil)
)

const defaultAssertionPoolMaxGwei = uint64(1000000000) // 1 ETH.

// The Manager struct is responsible for several tasks related to the assertion chain:
// 1. It continuously polls the assertion chain to check for posted, on-chain assertions starting from the latest confirmed assertion up to the newest one.
// 2. As the assertion chain advances, the Manager keeps polling to stay updated.
Expand Down Expand Up @@ -70,6 +72,16 @@ type Manager struct {
startPostingSignal chan struct{}
layerZeroHeightsCache *protocol.LayerZeroHeights
layerZeroHeightsCacheLock sync.RWMutex
poolingConfig *AssertionPoolingConfig
}

type AssertionPoolingConfig struct {
Enable bool // Whether or not to enable the use of trustless assertion bond pools.
// The max amount of gwei to deposit into an assertion bond pool at a time. If 0, then a warning will be logged
// and no amount will be pooled.
MaxGweiToPool uint64
AssertionPoolCreatorFactoryAddr common.Address // The address of the assertion bonding pool creator contract.
PoolingTxOpts *bind.TransactOpts
}

type assertionChainData struct {
Expand All @@ -92,6 +104,18 @@ func WithDangerousReadyToPost() Opt {
}
}

func WithAssertionPoolCreatorFactoryAddr(addr common.Address) Opt {
return func(m *Manager) {
m.poolingConfig.AssertionPoolCreatorFactoryAddr = addr
}
}

func WithPoolingTxOpts(opts *bind.TransactOpts) Opt {
return func(m *Manager) {
m.poolingConfig.PoolingTxOpts = opts
}
}

// NewManager creates a manager from the required dependencies.
func NewManager(
chain protocol.AssertionChain,
Expand Down Expand Up @@ -140,6 +164,10 @@ func NewManager(
observedCanonicalAssertions: make(chan protocol.AssertionHash, 1000),
isReadyToPost: false,
startPostingSignal: make(chan struct{}),
poolingConfig: &AssertionPoolingConfig{
Enable: true, // Enable the use of assertion bonding pools by default.
MaxGweiToPool: defaultAssertionPoolMaxGwei,
},
}
for _, o := range opts {
o(m)
Expand Down
230 changes: 230 additions & 0 deletions assertions/trustless_bond_pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package assertions

import (
"context"
"math/big"
"time"

protocol "github.com/OffchainLabs/bold/chain-abstraction"
"github.com/OffchainLabs/bold/containers/option"
pools "github.com/OffchainLabs/bold/solgen/go/assertionStakingPoolgen"
"github.com/OffchainLabs/bold/solgen/go/mocksgen"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)

type useAssertionPoolArgs struct {
parentAssertion protocol.AssertionHash
assertionHash protocol.AssertionHash
}

func (m *Manager) useAssertionPool(
ctx context.Context,
args useAssertionPoolArgs,
) protocol.Assertion {
poolFactory, err := pools.NewAssertionStakingPoolCreator(
m.poolingConfig.AssertionPoolCreatorFactoryAddr,
m.backend,
)
if err != nil {
panic(err)
}
assertionPool, err := m.getOrCreateAssertionPool(ctx, poolFactory, args)
if err != nil {
panic(err)
}
// Max parameter as a config into how much to deposit into the pool.
assertionPool.depositIntoPool(ctx, args)

// After this, we monitor the pool until it is ready to be posted.
assertionPool.waitUntilFunded(ctx, poolFactory, monitorPoolCreatorArgs{})

// Then, we can trigger the posting of the assertion.
return assertionPool.postAssertionToPool(ctx, poolFactory, monitorPoolCreatorArgs{})
}

// Get an assertion staking pool for the assertion we wish to post.
func (m *Manager) getOrCreateAssertionPool(
ctx context.Context,
factory *pools.AssertionStakingPoolCreator,
args useAssertionPoolArgs,
) (*assertionStakingPool, error) {
var poolCreation createdPool
poolOpt := m.checkAssertionPoolCreated(ctx, factory, args)
if poolOpt.IsSome() {
poolCreation = poolOpt.Unwrap()
} else {
poolCreation = m.createAssertionStakingPool(ctx, factory, args)
}
pool, err := pools.NewAssertionStakingPool(poolCreation.address, m.backend)
if err != nil {
return nil, err
}
return &assertionStakingPool{
cfg: m.poolingConfig,
addr: poolCreation.address,
assertionHash: poolCreation.assertionHash,
pool: pool,
createdAtBlock: poolCreation.createdAtBlock,
}, nil
}

type createdPool struct {
assertionHash common.Hash
address common.Address
createdAtBlock uint64
}

// Scan for any pools created since the latest confirmed assertion
// from the staking pool factory.
func (m *Manager) checkAssertionPoolCreated(
ctx context.Context,
factory *pools.AssertionStakingPoolCreator,
args useAssertionPoolArgs,
) option.Option[createdPool] {
parent, err := m.chain.ReadAssertionCreationInfo(ctx, args.parentAssertion)
if err != nil {
panic(err)
}
var query = ethereum.FilterQuery{
FromBlock: new(big.Int).SetUint64(parent.CreationBlock),
ToBlock: nil,
Addresses: []common.Address{m.rollupAddr},
Topics: [][]common.Hash{
{args.assertionHash.Hash},
},
}
logs, err := m.backend.FilterLogs(ctx, query)
if err != nil {
panic(err)
}
if len(logs) != 1 {
return option.None[createdPool]()
}
poolDetails, err := factory.ParseNewAssertionPoolCreated(logs[0])
if err != nil {
panic(err)
}
return option.Some(createdPool{
assertionHash: poolDetails.AssertionHash,
address: poolDetails.AssertionPool,
createdAtBlock: poolDetails.Raw.BlockNumber,
})
}

func (m *Manager) createAssertionStakingPool(
ctx context.Context,
factory *pools.AssertionStakingPoolCreator,
args useAssertionPoolArgs,
) createdPool {
// TODO: Do this through the chain abstraction...
// Get the receipt and the address of the new pool.
// factory.CreatePool(m.poolingConfig.PoolingTxOpts, m.rollupAddr, args.assertionHash.Hash)
return createdPool{
assertionHash: args.assertionHash.Hash,
// address: ,
}
}

type assertionStakingPool struct {
cfg *AssertionPoolingConfig
backend protocol.ChainBackend
addr common.Address
assertionHash common.Hash
pool *pools.AssertionStakingPool
createdAtBlock uint64
}

type monitorPoolCreatorArgs struct {
parentAssertion protocol.AssertionHash
assertionHash protocol.AssertionHash
}

func (p *assertionStakingPool) depositIntoPool(
ctx context.Context,
args useAssertionPoolArgs,
) {
gweiToDeposit := new(big.Int).SetUint64(p.cfg.MaxGweiToPool)
gweiToWei := big.NewInt(1e9) // 10^9
weiToDeposit := new(big.Int).Mul(gweiToDeposit, gweiToWei)
_, _ = p.pool.DepositIntoPool(p.cfg.PoolingTxOpts, weiToDeposit)
}

// Monitor any staking pools that we care about which reach the threshold.
// Should we block until the pool threshold is reached and not post more?
// There is only one canonical assertion branch, so yes we likely do have to wait.
func (p *assertionStakingPool) waitUntilFunded(
ctx context.Context,
factory *pools.AssertionStakingPoolCreator,
args monitorPoolCreatorArgs,
) {
fromBlock := p.createdAtBlock
latestBlock, err := p.backend.HeaderByNumber(ctx, nil) // TODO: Get desired block number.
if err != nil {
panic(err)
}
tokenAddr, err := p.pool.StakeToken(&bind.CallOpts{Context: ctx}) // TODO: Get desired block number.
if err != nil {
panic(err)
}
stakeToken, err := mocksgen.NewTestWETH9(tokenAddr, p.backend) // TODO: Do not use the mock here, just use an ierc20 binding.
if err != nil {
panic(err)
}
bal, err := stakeToken.BalanceOf(&bind.CallOpts{Context: ctx}, p.addr) // TODO: Get desired block number.
if err != nil {
panic(err)
}

// If balance is already enough, return.
_ = bal

ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if !latestBlock.Number.IsUint64() {
log.Error("latest block header number is not a uint64")
continue
}
toBlock := latestBlock.Number.Uint64()
if fromBlock == toBlock {
continue
}
// Store the parent assertion details and use that to determine if funded.
// Scan for all deposits into the pool until the base stake threshold is reached.
// Use the base stake from the parent assertion as the factor here.
filterOpts := &bind.FilterOpts{
Context: ctx,
Start: p.createdAtBlock,
End: nil,
}
it, err := p.pool.FilterStakeDeposited(filterOpts, nil)
if err != nil {
panic(err)
}
for it.Next() {
// Get the balance after the deposit at that block number.
// If balance reached...
if true {
return
}
}
case <-ctx.Done():
// TODO: Return an error.
return
}
}
}

func (p *assertionStakingPool) postAssertionToPool(
ctx context.Context,
factory *pools.AssertionStakingPoolCreator,
args monitorPoolCreatorArgs,
) protocol.Assertion {
// p.pool.CreateAssertion(p.cfg.PoolingTxOpts, args.Hash)
return nil
}
1 change: 1 addition & 0 deletions assertions/trustless_bond_pool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package assertions
Loading
Loading