-
Notifications
You must be signed in to change notification settings - Fork 468
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 external signer for dataposter #1919
Changes from 14 commits
a619d43
62801eb
4269470
c3ed563
c177f02
fa2f6fa
2f7b867
cfb4bc7
c27cb4b
73eb315
4661888
3816204
c551ff5
412a7e0
34fe2d0
1e9b588
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,10 +15,12 @@ import ( | |
|
||
"github.com/ethereum/go-ethereum/accounts/abi/bind" | ||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/ethereum/go-ethereum/common/hexutil" | ||
"github.com/ethereum/go-ethereum/core/types" | ||
"github.com/ethereum/go-ethereum/ethdb" | ||
"github.com/ethereum/go-ethereum/log" | ||
"github.com/ethereum/go-ethereum/params" | ||
"github.com/ethereum/go-ethereum/rlp" | ||
"github.com/ethereum/go-ethereum/rpc" | ||
"github.com/go-redis/redis/v8" | ||
"github.com/offchainlabs/nitro/arbnode/dataposter/dbstorage" | ||
|
@@ -47,7 +49,7 @@ type DataPoster struct { | |
headerReader *headerreader.HeaderReader | ||
client arbutil.L1Interface | ||
sender common.Address | ||
signer bind.SignerFn | ||
signer signerFn | ||
redisLock AttemptLocker | ||
config ConfigFetcher | ||
replacementTimes []time.Duration | ||
|
@@ -66,6 +68,11 @@ type DataPoster struct { | |
errorCount map[uint64]int // number of consecutive intermittent errors rbf-ing or sending, per nonce | ||
} | ||
|
||
// signerFn is a signer function callback when a contract requires a method to | ||
// sign the transaction before submission. | ||
// This can be local or external, hence the context parameter. | ||
type signerFn func(context.Context, common.Address, *types.Transaction) (*types.Transaction, error) | ||
|
||
type AttemptLocker interface { | ||
AttemptLock(context.Context) bool | ||
} | ||
|
@@ -85,7 +92,7 @@ func parseReplacementTimes(val string) ([]time.Duration, error) { | |
lastReplacementTime = t | ||
} | ||
if len(res) == 0 { | ||
log.Warn("disabling replace-by-fee for data poster") | ||
log.Warn("Disabling replace-by-fee for data poster") | ||
} | ||
// To avoid special casing "don't replace again", replace in 10 years. | ||
return append(res, time.Hour*24*365*10), nil | ||
|
@@ -103,13 +110,13 @@ type DataPosterOpts struct { | |
} | ||
|
||
func NewDataPoster(ctx context.Context, opts *DataPosterOpts) (*DataPoster, error) { | ||
initConfig := opts.Config() | ||
replacementTimes, err := parseReplacementTimes(initConfig.ReplacementTimes) | ||
cfg := opts.Config() | ||
replacementTimes, err := parseReplacementTimes(cfg.ReplacementTimes) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if opts.HeaderReader.IsParentChainArbitrum() && !initConfig.UseNoOpStorage { | ||
initConfig.UseNoOpStorage = true | ||
if opts.HeaderReader.IsParentChainArbitrum() && !cfg.UseNoOpStorage { | ||
cfg.UseNoOpStorage = true | ||
log.Info("Disabling data poster storage, as parent chain appears to be an Arbitrum chain without a mempool") | ||
} | ||
encF := func() storage.EncoderDecoderInterface { | ||
|
@@ -120,17 +127,17 @@ func NewDataPoster(ctx context.Context, opts *DataPosterOpts) (*DataPoster, erro | |
} | ||
var queue QueueStorage | ||
switch { | ||
case initConfig.UseNoOpStorage: | ||
case cfg.UseNoOpStorage: | ||
queue = &noop.Storage{} | ||
case opts.RedisClient != nil: | ||
var err error | ||
queue, err = redisstorage.NewStorage(opts.RedisClient, opts.RedisKey, &initConfig.RedisSigner, encF) | ||
queue, err = redisstorage.NewStorage(opts.RedisClient, opts.RedisKey, &cfg.RedisSigner, encF) | ||
if err != nil { | ||
return nil, err | ||
} | ||
case initConfig.UseDBStorage: | ||
case cfg.UseDBStorage: | ||
storage := dbstorage.New(opts.Database, func() storage.EncoderDecoderInterface { return &storage.EncoderDecoder{} }) | ||
if initConfig.Dangerous.ClearDBStorage { | ||
if cfg.Dangerous.ClearDBStorage { | ||
if err := storage.PruneAll(ctx); err != nil { | ||
return nil, err | ||
} | ||
|
@@ -139,18 +146,64 @@ func NewDataPoster(ctx context.Context, opts *DataPosterOpts) (*DataPoster, erro | |
default: | ||
queue = slice.NewStorage(func() storage.EncoderDecoderInterface { return &storage.EncoderDecoder{} }) | ||
} | ||
return &DataPoster{ | ||
headerReader: opts.HeaderReader, | ||
client: opts.HeaderReader.Client(), | ||
sender: opts.Auth.From, | ||
signer: opts.Auth.Signer, | ||
dp := &DataPoster{ | ||
headerReader: opts.HeaderReader, | ||
client: opts.HeaderReader.Client(), | ||
sender: opts.Auth.From, | ||
signer: func(_ context.Context, addr common.Address, tx *types.Transaction) (*types.Transaction, error) { | ||
return opts.Auth.Signer(addr, tx) | ||
}, | ||
config: opts.Config, | ||
replacementTimes: replacementTimes, | ||
metadataRetriever: opts.MetadataRetriever, | ||
queue: queue, | ||
redisLock: opts.RedisLock, | ||
errorCount: make(map[uint64]int), | ||
}, nil | ||
} | ||
if cfg.ExternalSigner.URL != "" { | ||
signer, sender, err := externalSigner(ctx, &cfg.ExternalSigner) | ||
if err != nil { | ||
return nil, err | ||
} | ||
dp.signer, dp.sender = signer, sender | ||
} | ||
return dp, nil | ||
} | ||
|
||
// externalSigner returns signer function and ethereum address of the signer. | ||
// Returns an error if address isn't specified or if it can't connect to the | ||
// signer RPC server. | ||
func externalSigner(ctx context.Context, opts *ExternalSignerCfg) (signerFn, common.Address, error) { | ||
if opts.Address == "" { | ||
return nil, common.Address{}, errors.New("external signer (From) address specified") | ||
} | ||
sender := common.HexToAddress(opts.Address) | ||
client, err := rpc.DialContext(ctx, opts.URL) | ||
if err != nil { | ||
return nil, common.Address{}, fmt.Errorf("error connecting external signer: %w", err) | ||
} | ||
|
||
var hasher types.Signer | ||
return func(ctx context.Context, addr common.Address, tx *types.Transaction) (*types.Transaction, error) { | ||
// According to the "eth_signTransaction" API definition, this should be | ||
// RLP encoded transaction object. | ||
// https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_signtransaction | ||
var data hexutil.Bytes | ||
if err := client.CallContext(ctx, &data, opts.Method, tx); err != nil { | ||
return nil, fmt.Errorf("signing transaction: %w", err) | ||
} | ||
var signedTx types.Transaction | ||
if err := rlp.DecodeBytes(data, &signedTx); err != nil { | ||
return nil, fmt.Errorf("error decoding signed transaction: %w", err) | ||
} | ||
if hasher == nil { | ||
hasher = types.LatestSignerForChainID(tx.ChainId()) | ||
} | ||
if hasher.Hash(tx) != hasher.Hash(&signedTx) { | ||
return nil, fmt.Errorf("transaction: %x from external signer differs from request: %x", hasher.Hash(&signedTx), hasher.Hash(tx)) | ||
} | ||
return &signedTx, nil | ||
}, sender, nil | ||
} | ||
|
||
func (p *DataPoster) Sender() common.Address { | ||
|
@@ -371,7 +424,7 @@ func (p *DataPoster) PostTransaction(ctx context.Context, dataCreatedAt time.Tim | |
Data: calldata, | ||
AccessList: accessList, | ||
} | ||
fullTx, err := p.signer(p.sender, types.NewTx(&inner)) | ||
fullTx, err := p.signer(ctx, p.sender, types.NewTx(&inner)) | ||
if err != nil { | ||
return nil, fmt.Errorf("signing transaction: %w", err) | ||
} | ||
|
@@ -450,7 +503,7 @@ func (p *DataPoster) replaceTx(ctx context.Context, prevTx *storage.QueuedTransa | |
newTx.Sent = false | ||
newTx.Data.GasFeeCap = newFeeCap | ||
newTx.Data.GasTipCap = newTipCap | ||
newTx.FullTx, err = p.signer(p.sender, types.NewTx(&newTx.Data)) | ||
newTx.FullTx, err = p.signer(ctx, p.sender, types.NewTx(&newTx.Data)) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -636,20 +689,32 @@ type DataPosterConfig struct { | |
ReplacementTimes string `koanf:"replacement-times"` | ||
// This is forcibly disabled if the parent chain is an Arbitrum chain, | ||
// so you should probably use DataPoster's waitForL1Finality method instead of reading this field directly. | ||
WaitForL1Finality bool `koanf:"wait-for-l1-finality" reload:"hot"` | ||
MaxMempoolTransactions uint64 `koanf:"max-mempool-transactions" reload:"hot"` | ||
MaxQueuedTransactions int `koanf:"max-queued-transactions" reload:"hot"` | ||
TargetPriceGwei float64 `koanf:"target-price-gwei" reload:"hot"` | ||
UrgencyGwei float64 `koanf:"urgency-gwei" reload:"hot"` | ||
MinFeeCapGwei float64 `koanf:"min-fee-cap-gwei" reload:"hot"` | ||
MinTipCapGwei float64 `koanf:"min-tip-cap-gwei" reload:"hot"` | ||
MaxTipCapGwei float64 `koanf:"max-tip-cap-gwei" reload:"hot"` | ||
NonceRbfSoftConfs uint64 `koanf:"nonce-rbf-soft-confs" reload:"hot"` | ||
AllocateMempoolBalance bool `koanf:"allocate-mempool-balance" reload:"hot"` | ||
UseDBStorage bool `koanf:"use-db-storage"` | ||
UseNoOpStorage bool `koanf:"use-noop-storage"` | ||
LegacyStorageEncoding bool `koanf:"legacy-storage-encoding" reload:"hot"` | ||
Dangerous DangerousConfig `koanf:"dangerous"` | ||
WaitForL1Finality bool `koanf:"wait-for-l1-finality" reload:"hot"` | ||
MaxMempoolTransactions uint64 `koanf:"max-mempool-transactions" reload:"hot"` | ||
MaxQueuedTransactions int `koanf:"max-queued-transactions" reload:"hot"` | ||
TargetPriceGwei float64 `koanf:"target-price-gwei" reload:"hot"` | ||
UrgencyGwei float64 `koanf:"urgency-gwei" reload:"hot"` | ||
MinFeeCapGwei float64 `koanf:"min-fee-cap-gwei" reload:"hot"` | ||
MinTipCapGwei float64 `koanf:"min-tip-cap-gwei" reload:"hot"` | ||
MaxTipCapGwei float64 `koanf:"max-tip-cap-gwei" reload:"hot"` | ||
NonceRbfSoftConfs uint64 `koanf:"nonce-rbf-soft-confs" reload:"hot"` | ||
AllocateMempoolBalance bool `koanf:"allocate-mempool-balance" reload:"hot"` | ||
UseDBStorage bool `koanf:"use-db-storage"` | ||
UseNoOpStorage bool `koanf:"use-noop-storage"` | ||
LegacyStorageEncoding bool `koanf:"legacy-storage-encoding" reload:"hot"` | ||
Dangerous DangerousConfig `koanf:"dangerous"` | ||
ExternalSigner ExternalSignerCfg `koanf:"external-signer"` | ||
} | ||
|
||
type ExternalSignerCfg struct { | ||
// URL of the external signer rpc server, if set this overrides transaction | ||
// options and uses external signer | ||
// for signing transactions. | ||
URL string `koanf:"url"` | ||
// Hex encoded ethereum address of the external signer. | ||
Address string `koanf:"address"` | ||
// API method name (e.g. eth_signTransaction). | ||
Method string `koanf:"method"` | ||
} | ||
|
||
type DangerousConfig struct { | ||
|
@@ -680,12 +745,19 @@ func DataPosterConfigAddOptions(prefix string, f *pflag.FlagSet, defaultDataPost | |
|
||
signature.SimpleHmacConfigAddOptions(prefix+".redis-signer", f) | ||
addDangerousOptions(prefix+".dangerous", f) | ||
addExternalSignerOptions(prefix+".external-signer", f) | ||
} | ||
|
||
func addDangerousOptions(prefix string, f *pflag.FlagSet) { | ||
f.Bool(prefix+".clear-dbstorage", DefaultDataPosterConfig.Dangerous.ClearDBStorage, "clear database storage") | ||
} | ||
|
||
func addExternalSignerOptions(prefix string, f *pflag.FlagSet) { | ||
f.String(prefix+".url", DefaultDataPosterConfig.ExternalSigner.URL, "external signer url") | ||
f.String(prefix+".address", DefaultDataPosterConfig.ExternalSigner.Address, "external signer address") | ||
f.String(prefix+".method", DefaultDataPosterConfig.ExternalSigner.Method, "external signer method") | ||
} | ||
|
||
var DefaultDataPosterConfig = DataPosterConfig{ | ||
ReplacementTimes: "5m,10m,20m,30m,1h,2h,4h,6h,8h,12h,16h,18h,20h,22h", | ||
WaitForL1Finality: true, | ||
|
@@ -700,6 +772,7 @@ var DefaultDataPosterConfig = DataPosterConfig{ | |
UseNoOpStorage: false, | ||
LegacyStorageEncoding: true, | ||
Dangerous: DangerousConfig{ClearDBStorage: false}, | ||
ExternalSigner: ExternalSignerCfg{}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should still keep the default method as eth_signTransaction as that seems to be the most common method name (also applies to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. ( I guess you meant There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
|
||
var DefaultDataPosterConfigForValidator = func() DataPosterConfig { | ||
|
@@ -721,6 +794,7 @@ var TestDataPosterConfig = DataPosterConfig{ | |
AllocateMempoolBalance: true, | ||
UseDBStorage: false, | ||
UseNoOpStorage: false, | ||
ExternalSigner: ExternalSignerCfg{}, | ||
} | ||
|
||
var TestDataPosterConfigForValidator = func() DataPosterConfig { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to add a safety check that this is the same transaction we requested to be signed. I think there's potential security issues without that, where a remote signer MITM returns a different tx with a bad signature, and then convinces the data poster to request that different tx be signed against the real remote signer.
The ideal way to do this would be to get a geth
types.Signer
(really confusingly named, it's more of a signature verifier than a signer, it doesn't have a private key or anything), and use its Hash method which returns the pre-signature hash, to verify that aside from the signature the txs are the same. I think we can get a "signer" by callingtypes.LatestSignerForChainID
with the parent chain chain id from the config (we'll probably add that as a constructor argument to the data poster).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! Added the check, although doing the lazy initialization, otherwise passing the signer (or chain ID) all the way from nitro.go would unnecessarily pollute call chain.