mirror of https://github.com/ethereum/go-ethereum
commit
da6cdaf635
@ -0,0 +1,23 @@ |
||||
name: i386 linux tests |
||||
|
||||
on: |
||||
push: |
||||
branches: [ master ] |
||||
pull_request: |
||||
branches: [ master ] |
||||
workflow_dispatch: |
||||
|
||||
jobs: |
||||
build: |
||||
runs-on: self-hosted |
||||
steps: |
||||
- uses: actions/checkout@v2 |
||||
- name: Set up Go |
||||
uses: actions/setup-go@v2 |
||||
with: |
||||
go-version: 1.21.4 |
||||
- name: Run tests |
||||
run: go test ./... |
||||
env: |
||||
GOOS: linux |
||||
GOARCH: 386 |
@ -0,0 +1,125 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package light |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"fmt" |
||||
|
||||
"github.com/ethereum/go-ethereum/common/lru" |
||||
"github.com/ethereum/go-ethereum/ethdb" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
) |
||||
|
||||
// canonicalStore stores instances of the given type in a database and caches
|
||||
// them in memory, associated with a continuous range of period numbers.
|
||||
// Note: canonicalStore is not thread safe and it is the caller's responsibility
|
||||
// to avoid concurrent access.
|
||||
type canonicalStore[T any] struct { |
||||
keyPrefix []byte |
||||
periods periodRange |
||||
cache *lru.Cache[uint64, T] |
||||
} |
||||
|
||||
// newCanonicalStore creates a new canonicalStore and loads all keys associated
|
||||
// with the keyPrefix in order to determine the ranges available in the database.
|
||||
func newCanonicalStore[T any](db ethdb.Iteratee, keyPrefix []byte) (*canonicalStore[T], error) { |
||||
cs := &canonicalStore[T]{ |
||||
keyPrefix: keyPrefix, |
||||
cache: lru.NewCache[uint64, T](100), |
||||
} |
||||
var ( |
||||
iter = db.NewIterator(keyPrefix, nil) |
||||
kl = len(keyPrefix) |
||||
first = true |
||||
) |
||||
defer iter.Release() |
||||
|
||||
for iter.Next() { |
||||
if len(iter.Key()) != kl+8 { |
||||
log.Warn("Invalid key length in the canonical chain database", "key", fmt.Sprintf("%#x", iter.Key())) |
||||
continue |
||||
} |
||||
period := binary.BigEndian.Uint64(iter.Key()[kl : kl+8]) |
||||
if first { |
||||
cs.periods.Start = period |
||||
} else if cs.periods.End != period { |
||||
return nil, fmt.Errorf("gap in the canonical chain database between periods %d and %d", cs.periods.End, period-1) |
||||
} |
||||
first = false |
||||
cs.periods.End = period + 1 |
||||
} |
||||
return cs, nil |
||||
} |
||||
|
||||
// databaseKey returns the database key belonging to the given period.
|
||||
func (cs *canonicalStore[T]) databaseKey(period uint64) []byte { |
||||
return binary.BigEndian.AppendUint64(append([]byte{}, cs.keyPrefix...), period) |
||||
} |
||||
|
||||
// add adds the given item to the database. It also ensures that the range remains
|
||||
// continuous. Can be used either with a batch or database backend.
|
||||
func (cs *canonicalStore[T]) add(backend ethdb.KeyValueWriter, period uint64, value T) error { |
||||
if !cs.periods.canExpand(period) { |
||||
return fmt.Errorf("period expansion is not allowed, first: %d, next: %d, period: %d", cs.periods.Start, cs.periods.End, period) |
||||
} |
||||
enc, err := rlp.EncodeToBytes(value) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if err := backend.Put(cs.databaseKey(period), enc); err != nil { |
||||
return err |
||||
} |
||||
cs.cache.Add(period, value) |
||||
cs.periods.expand(period) |
||||
return nil |
||||
} |
||||
|
||||
// deleteFrom removes items starting from the given period.
|
||||
func (cs *canonicalStore[T]) deleteFrom(db ethdb.KeyValueWriter, fromPeriod uint64) (deleted periodRange) { |
||||
keepRange, deleteRange := cs.periods.split(fromPeriod) |
||||
deleteRange.each(func(period uint64) { |
||||
db.Delete(cs.databaseKey(period)) |
||||
cs.cache.Remove(period) |
||||
}) |
||||
cs.periods = keepRange |
||||
return deleteRange |
||||
} |
||||
|
||||
// get returns the item at the given period or the null value of the given type
|
||||
// if no item is present.
|
||||
func (cs *canonicalStore[T]) get(backend ethdb.KeyValueReader, period uint64) (T, bool) { |
||||
var null, value T |
||||
if !cs.periods.contains(period) { |
||||
return null, false |
||||
} |
||||
if value, ok := cs.cache.Get(period); ok { |
||||
return value, true |
||||
} |
||||
enc, err := backend.Get(cs.databaseKey(period)) |
||||
if err != nil { |
||||
log.Error("Canonical store value not found", "period", period, "start", cs.periods.Start, "end", cs.periods.End) |
||||
return null, false |
||||
} |
||||
if err := rlp.DecodeBytes(enc, &value); err != nil { |
||||
log.Error("Error decoding canonical store value", "error", err) |
||||
return null, false |
||||
} |
||||
cs.cache.Add(period, value) |
||||
return value, true |
||||
} |
@ -0,0 +1,514 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package light |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"math" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/params" |
||||
"github.com/ethereum/go-ethereum/beacon/types" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/lru" |
||||
"github.com/ethereum/go-ethereum/common/mclock" |
||||
"github.com/ethereum/go-ethereum/core/rawdb" |
||||
"github.com/ethereum/go-ethereum/ethdb" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
var ( |
||||
ErrNeedCommittee = errors.New("sync committee required") |
||||
ErrInvalidUpdate = errors.New("invalid committee update") |
||||
ErrInvalidPeriod = errors.New("invalid update period") |
||||
ErrWrongCommitteeRoot = errors.New("wrong committee root") |
||||
ErrCannotReorg = errors.New("can not reorg committee chain") |
||||
) |
||||
|
||||
// CommitteeChain is a passive data structure that can validate, hold and update
|
||||
// a chain of beacon light sync committees and updates. It requires at least one
|
||||
// externally set fixed committee root at the beginning of the chain which can
|
||||
// be set either based on a BootstrapData or a trusted source (a local beacon
|
||||
// full node). This makes the structure useful for both light client and light
|
||||
// server setups.
|
||||
//
|
||||
// It always maintains the following consistency constraints:
|
||||
// - a committee can only be present if its root hash matches an existing fixed
|
||||
// root or if it is proven by an update at the previous period
|
||||
// - an update can only be present if a committee is present at the same period
|
||||
// and the update signature is valid and has enough participants.
|
||||
// The committee at the next period (proven by the update) should also be
|
||||
// present (note that this means they can only be added together if neither
|
||||
// is present yet). If a fixed root is present at the next period then the
|
||||
// update can only be present if it proves the same committee root.
|
||||
//
|
||||
// Once synced to the current sync period, CommitteeChain can also validate
|
||||
// signed beacon headers.
|
||||
type CommitteeChain struct { |
||||
// chainmu guards against concurrent access to the canonicalStore structures
|
||||
// (updates, committees, fixedCommitteeRoots) and ensures that they stay consistent
|
||||
// with each other and with committeeCache.
|
||||
chainmu sync.RWMutex |
||||
db ethdb.KeyValueStore |
||||
updates *canonicalStore[*types.LightClientUpdate] |
||||
committees *canonicalStore[*types.SerializedSyncCommittee] |
||||
fixedCommitteeRoots *canonicalStore[common.Hash] |
||||
committeeCache *lru.Cache[uint64, syncCommittee] // cache deserialized committees
|
||||
|
||||
clock mclock.Clock // monotonic clock (simulated clock in tests)
|
||||
unixNano func() int64 // system clock (simulated clock in tests)
|
||||
sigVerifier committeeSigVerifier // BLS sig verifier (dummy verifier in tests)
|
||||
|
||||
config *types.ChainConfig |
||||
signerThreshold int |
||||
minimumUpdateScore types.UpdateScore |
||||
enforceTime bool // enforceTime specifies whether the age of a signed header should be checked
|
||||
} |
||||
|
||||
// NewCommitteeChain creates a new CommitteeChain.
|
||||
func NewCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, signerThreshold int, enforceTime bool) *CommitteeChain { |
||||
return newCommitteeChain(db, config, signerThreshold, enforceTime, blsVerifier{}, &mclock.System{}, func() int64 { return time.Now().UnixNano() }) |
||||
} |
||||
|
||||
// newCommitteeChain creates a new CommitteeChain with the option of replacing the
|
||||
// clock source and signature verification for testing purposes.
|
||||
func newCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, signerThreshold int, enforceTime bool, sigVerifier committeeSigVerifier, clock mclock.Clock, unixNano func() int64) *CommitteeChain { |
||||
s := &CommitteeChain{ |
||||
committeeCache: lru.NewCache[uint64, syncCommittee](10), |
||||
db: db, |
||||
sigVerifier: sigVerifier, |
||||
clock: clock, |
||||
unixNano: unixNano, |
||||
config: config, |
||||
signerThreshold: signerThreshold, |
||||
enforceTime: enforceTime, |
||||
minimumUpdateScore: types.UpdateScore{ |
||||
SignerCount: uint32(signerThreshold), |
||||
SubPeriodIndex: params.SyncPeriodLength / 16, |
||||
}, |
||||
} |
||||
|
||||
var err1, err2, err3 error |
||||
if s.fixedCommitteeRoots, err1 = newCanonicalStore[common.Hash](db, rawdb.FixedCommitteeRootKey); err1 != nil { |
||||
log.Error("Error creating fixed committee root store", "error", err1) |
||||
} |
||||
if s.committees, err2 = newCanonicalStore[*types.SerializedSyncCommittee](db, rawdb.SyncCommitteeKey); err2 != nil { |
||||
log.Error("Error creating committee store", "error", err2) |
||||
} |
||||
if s.updates, err3 = newCanonicalStore[*types.LightClientUpdate](db, rawdb.BestUpdateKey); err3 != nil { |
||||
log.Error("Error creating update store", "error", err3) |
||||
} |
||||
if err1 != nil || err2 != nil || err3 != nil || !s.checkConstraints() { |
||||
log.Info("Resetting invalid committee chain") |
||||
s.Reset() |
||||
} |
||||
// roll back invalid updates (might be necessary if forks have been changed since last time)
|
||||
for !s.updates.periods.isEmpty() { |
||||
update, ok := s.updates.get(s.db, s.updates.periods.End-1) |
||||
if !ok { |
||||
log.Error("Sync committee update missing", "period", s.updates.periods.End-1) |
||||
s.Reset() |
||||
break |
||||
} |
||||
if valid, err := s.verifyUpdate(update); err != nil { |
||||
log.Error("Error validating update", "period", s.updates.periods.End-1, "error", err) |
||||
} else if valid { |
||||
break |
||||
} |
||||
if err := s.rollback(s.updates.periods.End); err != nil { |
||||
log.Error("Error writing batch into chain database", "error", err) |
||||
} |
||||
} |
||||
if !s.committees.periods.isEmpty() { |
||||
log.Trace("Sync committee chain loaded", "first period", s.committees.periods.Start, "last period", s.committees.periods.End-1) |
||||
} |
||||
return s |
||||
} |
||||
|
||||
// checkConstraints checks committee chain validity constraints
|
||||
func (s *CommitteeChain) checkConstraints() bool { |
||||
isNotInFixedCommitteeRootRange := func(r periodRange) bool { |
||||
return s.fixedCommitteeRoots.periods.isEmpty() || |
||||
r.Start < s.fixedCommitteeRoots.periods.Start || |
||||
r.Start >= s.fixedCommitteeRoots.periods.End |
||||
} |
||||
|
||||
valid := true |
||||
if !s.updates.periods.isEmpty() { |
||||
if isNotInFixedCommitteeRootRange(s.updates.periods) { |
||||
log.Error("Start update is not in the fixed roots range") |
||||
valid = false |
||||
} |
||||
if s.committees.periods.Start > s.updates.periods.Start || s.committees.periods.End <= s.updates.periods.End { |
||||
log.Error("Missing committees in update range") |
||||
valid = false |
||||
} |
||||
} |
||||
if !s.committees.periods.isEmpty() { |
||||
if isNotInFixedCommitteeRootRange(s.committees.periods) { |
||||
log.Error("Start committee is not in the fixed roots range") |
||||
valid = false |
||||
} |
||||
if s.committees.periods.End > s.fixedCommitteeRoots.periods.End && s.committees.periods.End > s.updates.periods.End+1 { |
||||
log.Error("Last committee is neither in the fixed roots range nor proven by updates") |
||||
valid = false |
||||
} |
||||
} |
||||
return valid |
||||
} |
||||
|
||||
// Reset resets the committee chain.
|
||||
func (s *CommitteeChain) Reset() { |
||||
s.chainmu.Lock() |
||||
defer s.chainmu.Unlock() |
||||
|
||||
if err := s.rollback(0); err != nil { |
||||
log.Error("Error writing batch into chain database", "error", err) |
||||
} |
||||
} |
||||
|
||||
// CheckpointInit initializes a CommitteeChain based on the checkpoint.
|
||||
// Note: if the chain is already initialized and the committees proven by the
|
||||
// checkpoint do match the existing chain then the chain is retained and the
|
||||
// new checkpoint becomes fixed.
|
||||
func (s *CommitteeChain) CheckpointInit(bootstrap *types.BootstrapData) error { |
||||
s.chainmu.Lock() |
||||
defer s.chainmu.Unlock() |
||||
|
||||
if err := bootstrap.Validate(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
period := bootstrap.Header.SyncPeriod() |
||||
if err := s.deleteFixedCommitteeRootsFrom(period + 2); err != nil { |
||||
s.Reset() |
||||
return err |
||||
} |
||||
if s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot) != nil { |
||||
s.Reset() |
||||
if err := s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot); err != nil { |
||||
s.Reset() |
||||
return err |
||||
} |
||||
} |
||||
if err := s.addFixedCommitteeRoot(period+1, common.Hash(bootstrap.CommitteeBranch[0])); err != nil { |
||||
s.Reset() |
||||
return err |
||||
} |
||||
if err := s.addCommittee(period, bootstrap.Committee); err != nil { |
||||
s.Reset() |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// addFixedCommitteeRoot sets a fixed committee root at the given period.
|
||||
// Note that the period where the first committee is added has to have a fixed
|
||||
// root which can either come from a BootstrapData or a trusted source.
|
||||
func (s *CommitteeChain) addFixedCommitteeRoot(period uint64, root common.Hash) error { |
||||
if root == (common.Hash{}) { |
||||
return ErrWrongCommitteeRoot |
||||
} |
||||
|
||||
batch := s.db.NewBatch() |
||||
oldRoot := s.getCommitteeRoot(period) |
||||
if !s.fixedCommitteeRoots.periods.canExpand(period) { |
||||
// Note: the fixed committee root range should always be continuous and
|
||||
// therefore the expected syncing method is to forward sync and optionally
|
||||
// backward sync periods one by one, starting from a checkpoint. The only
|
||||
// case when a root that is not adjacent to the already fixed ones can be
|
||||
// fixed is when the same root has already been proven by an update chain.
|
||||
// In this case the all roots in between can and should be fixed.
|
||||
// This scenario makes sense when a new trusted checkpoint is added to an
|
||||
// existing chain, ensuring that it will not be rolled back (might be
|
||||
// important in case of low signer participation rate).
|
||||
if root != oldRoot { |
||||
return ErrInvalidPeriod |
||||
} |
||||
// if the old root exists and matches the new one then it is guaranteed
|
||||
// that the given period is after the existing fixed range and the roots
|
||||
// in between can also be fixed.
|
||||
for p := s.fixedCommitteeRoots.periods.End; p < period; p++ { |
||||
if err := s.fixedCommitteeRoots.add(batch, p, s.getCommitteeRoot(p)); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
if oldRoot != (common.Hash{}) && (oldRoot != root) { |
||||
// existing old root was different, we have to reorg the chain
|
||||
if err := s.rollback(period); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
if err := s.fixedCommitteeRoots.add(batch, period, root); err != nil { |
||||
return err |
||||
} |
||||
if err := batch.Write(); err != nil { |
||||
log.Error("Error writing batch into chain database", "error", err) |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// deleteFixedCommitteeRootsFrom deletes fixed roots starting from the given period.
|
||||
// It also maintains chain consistency, meaning that it also deletes updates and
|
||||
// committees if they are no longer supported by a valid update chain.
|
||||
func (s *CommitteeChain) deleteFixedCommitteeRootsFrom(period uint64) error { |
||||
if period >= s.fixedCommitteeRoots.periods.End { |
||||
return nil |
||||
} |
||||
batch := s.db.NewBatch() |
||||
s.fixedCommitteeRoots.deleteFrom(batch, period) |
||||
if s.updates.periods.isEmpty() || period <= s.updates.periods.Start { |
||||
// Note: the first period of the update chain should always be fixed so if
|
||||
// the fixed root at the first update is removed then the entire update chain
|
||||
// and the proven committees have to be removed. Earlier committees in the
|
||||
// remaining fixed root range can stay.
|
||||
s.updates.deleteFrom(batch, period) |
||||
s.deleteCommitteesFrom(batch, period) |
||||
} else { |
||||
// The update chain stays intact, some previously fixed committee roots might
|
||||
// get unfixed but are still proven by the update chain. If there were
|
||||
// committees present after the range proven by updates, those should be
|
||||
// removed if the belonging fixed roots are also removed.
|
||||
fromPeriod := s.updates.periods.End + 1 // not proven by updates
|
||||
if period > fromPeriod { |
||||
fromPeriod = period // also not justified by fixed roots
|
||||
} |
||||
s.deleteCommitteesFrom(batch, fromPeriod) |
||||
} |
||||
if err := batch.Write(); err != nil { |
||||
log.Error("Error writing batch into chain database", "error", err) |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// deleteCommitteesFrom deletes committees starting from the given period.
|
||||
func (s *CommitteeChain) deleteCommitteesFrom(batch ethdb.Batch, period uint64) { |
||||
deleted := s.committees.deleteFrom(batch, period) |
||||
for period := deleted.Start; period < deleted.End; period++ { |
||||
s.committeeCache.Remove(period) |
||||
} |
||||
} |
||||
|
||||
// addCommittee adds a committee at the given period if possible.
|
||||
func (s *CommitteeChain) addCommittee(period uint64, committee *types.SerializedSyncCommittee) error { |
||||
if !s.committees.periods.canExpand(period) { |
||||
return ErrInvalidPeriod |
||||
} |
||||
root := s.getCommitteeRoot(period) |
||||
if root == (common.Hash{}) { |
||||
return ErrInvalidPeriod |
||||
} |
||||
if root != committee.Root() { |
||||
return ErrWrongCommitteeRoot |
||||
} |
||||
if !s.committees.periods.contains(period) { |
||||
if err := s.committees.add(s.db, period, committee); err != nil { |
||||
return err |
||||
} |
||||
s.committeeCache.Remove(period) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// InsertUpdate adds a new update if possible.
|
||||
func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error { |
||||
s.chainmu.Lock() |
||||
defer s.chainmu.Unlock() |
||||
|
||||
period := update.AttestedHeader.Header.SyncPeriod() |
||||
if !s.updates.periods.canExpand(period) || !s.committees.periods.contains(period) { |
||||
return ErrInvalidPeriod |
||||
} |
||||
if s.minimumUpdateScore.BetterThan(update.Score()) { |
||||
return ErrInvalidUpdate |
||||
} |
||||
oldRoot := s.getCommitteeRoot(period + 1) |
||||
reorg := oldRoot != (common.Hash{}) && oldRoot != update.NextSyncCommitteeRoot |
||||
if oldUpdate, ok := s.updates.get(s.db, period); ok && !update.Score().BetterThan(oldUpdate.Score()) { |
||||
// a better or equal update already exists; no changes, only fail if new one tried to reorg
|
||||
if reorg { |
||||
return ErrCannotReorg |
||||
} |
||||
return nil |
||||
} |
||||
if s.fixedCommitteeRoots.periods.contains(period+1) && reorg { |
||||
return ErrCannotReorg |
||||
} |
||||
if ok, err := s.verifyUpdate(update); err != nil { |
||||
return err |
||||
} else if !ok { |
||||
return ErrInvalidUpdate |
||||
} |
||||
addCommittee := !s.committees.periods.contains(period+1) || reorg |
||||
if addCommittee { |
||||
if nextCommittee == nil { |
||||
return ErrNeedCommittee |
||||
} |
||||
if nextCommittee.Root() != update.NextSyncCommitteeRoot { |
||||
return ErrWrongCommitteeRoot |
||||
} |
||||
} |
||||
if reorg { |
||||
if err := s.rollback(period + 1); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
batch := s.db.NewBatch() |
||||
if addCommittee { |
||||
if err := s.committees.add(batch, period+1, nextCommittee); err != nil { |
||||
return err |
||||
} |
||||
s.committeeCache.Remove(period + 1) |
||||
} |
||||
if err := s.updates.add(batch, period, update); err != nil { |
||||
return err |
||||
} |
||||
if err := batch.Write(); err != nil { |
||||
log.Error("Error writing batch into chain database", "error", err) |
||||
return err |
||||
} |
||||
log.Info("Inserted new committee update", "period", period, "next committee root", update.NextSyncCommitteeRoot) |
||||
return nil |
||||
} |
||||
|
||||
// NextSyncPeriod returns the next period where an update can be added and also
|
||||
// whether the chain is initialized at all.
|
||||
func (s *CommitteeChain) NextSyncPeriod() (uint64, bool) { |
||||
s.chainmu.RLock() |
||||
defer s.chainmu.RUnlock() |
||||
|
||||
if s.committees.periods.isEmpty() { |
||||
return 0, false |
||||
} |
||||
if !s.updates.periods.isEmpty() { |
||||
return s.updates.periods.End, true |
||||
} |
||||
return s.committees.periods.End - 1, true |
||||
} |
||||
|
||||
// rollback removes all committees and fixed roots from the given period and updates
|
||||
// starting from the previous period.
|
||||
func (s *CommitteeChain) rollback(period uint64) error { |
||||
max := s.updates.periods.End + 1 |
||||
if s.committees.periods.End > max { |
||||
max = s.committees.periods.End |
||||
} |
||||
if s.fixedCommitteeRoots.periods.End > max { |
||||
max = s.fixedCommitteeRoots.periods.End |
||||
} |
||||
for max > period { |
||||
max-- |
||||
batch := s.db.NewBatch() |
||||
s.deleteCommitteesFrom(batch, max) |
||||
s.fixedCommitteeRoots.deleteFrom(batch, max) |
||||
if max > 0 { |
||||
s.updates.deleteFrom(batch, max-1) |
||||
} |
||||
if err := batch.Write(); err != nil { |
||||
log.Error("Error writing batch into chain database", "error", err) |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// getCommitteeRoot returns the committee root at the given period, either fixed,
|
||||
// proven by a previous update or both. It returns an empty hash if the committee
|
||||
// root is unknown.
|
||||
func (s *CommitteeChain) getCommitteeRoot(period uint64) common.Hash { |
||||
if root, ok := s.fixedCommitteeRoots.get(s.db, period); ok || period == 0 { |
||||
return root |
||||
} |
||||
if update, ok := s.updates.get(s.db, period-1); ok { |
||||
return update.NextSyncCommitteeRoot |
||||
} |
||||
return common.Hash{} |
||||
} |
||||
|
||||
// getSyncCommittee returns the deserialized sync committee at the given period.
|
||||
func (s *CommitteeChain) getSyncCommittee(period uint64) (syncCommittee, error) { |
||||
if c, ok := s.committeeCache.Get(period); ok { |
||||
return c, nil |
||||
} |
||||
if sc, ok := s.committees.get(s.db, period); ok { |
||||
c, err := s.sigVerifier.deserializeSyncCommittee(sc) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("Sync committee #%d deserialization error: %v", period, err) |
||||
} |
||||
s.committeeCache.Add(period, c) |
||||
return c, nil |
||||
} |
||||
return nil, fmt.Errorf("Missing serialized sync committee #%d", period) |
||||
} |
||||
|
||||
// VerifySignedHeader returns true if the given signed header has a valid signature
|
||||
// according to the local committee chain. The caller should ensure that the
|
||||
// committees advertised by the same source where the signed header came from are
|
||||
// synced before verifying the signature.
|
||||
// The age of the header is also returned (the time elapsed since the beginning
|
||||
// of the given slot, according to the local system clock). If enforceTime is
|
||||
// true then negative age (future) headers are rejected.
|
||||
func (s *CommitteeChain) VerifySignedHeader(head types.SignedHeader) (bool, time.Duration, error) { |
||||
s.chainmu.RLock() |
||||
defer s.chainmu.RUnlock() |
||||
|
||||
return s.verifySignedHeader(head) |
||||
} |
||||
|
||||
func (s *CommitteeChain) verifySignedHeader(head types.SignedHeader) (bool, time.Duration, error) { |
||||
var age time.Duration |
||||
now := s.unixNano() |
||||
if head.Header.Slot < (uint64(now-math.MinInt64)/uint64(time.Second)-s.config.GenesisTime)/12 { |
||||
age = time.Duration(now - int64(time.Second)*int64(s.config.GenesisTime+head.Header.Slot*12)) |
||||
} else { |
||||
age = time.Duration(math.MinInt64) |
||||
} |
||||
if s.enforceTime && age < 0 { |
||||
return false, age, nil |
||||
} |
||||
committee, err := s.getSyncCommittee(types.SyncPeriod(head.SignatureSlot)) |
||||
if err != nil { |
||||
return false, 0, err |
||||
} |
||||
if committee == nil { |
||||
return false, age, nil |
||||
} |
||||
if signingRoot, err := s.config.Forks.SigningRoot(head.Header); err == nil { |
||||
return s.sigVerifier.verifySignature(committee, signingRoot, &head.Signature), age, nil |
||||
} |
||||
return false, age, nil |
||||
} |
||||
|
||||
// verifyUpdate checks whether the header signature is correct and the update
|
||||
// fits into the specified constraints (assumes that the update has been
|
||||
// successfully validated previously)
|
||||
func (s *CommitteeChain) verifyUpdate(update *types.LightClientUpdate) (bool, error) { |
||||
// Note: SignatureSlot determines the sync period of the committee used for signature
|
||||
// verification. Though in reality SignatureSlot is always bigger than update.Header.Slot,
|
||||
// setting them as equal here enforces the rule that they have to be in the same sync
|
||||
// period in order for the light client update proof to be meaningful.
|
||||
ok, age, err := s.verifySignedHeader(update.AttestedHeader) |
||||
if age < 0 { |
||||
log.Warn("Future committee update received", "age", age) |
||||
} |
||||
return ok, err |
||||
} |
@ -0,0 +1,356 @@ |
||||
// Copyright 2022 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package light |
||||
|
||||
import ( |
||||
"crypto/rand" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/params" |
||||
"github.com/ethereum/go-ethereum/beacon/types" |
||||
"github.com/ethereum/go-ethereum/common/mclock" |
||||
"github.com/ethereum/go-ethereum/ethdb/memorydb" |
||||
) |
||||
|
||||
var ( |
||||
testGenesis = newTestGenesis() |
||||
testGenesis2 = newTestGenesis() |
||||
|
||||
tfBase = newTestForks(testGenesis, types.Forks{ |
||||
&types.Fork{Epoch: 0, Version: []byte{0}}, |
||||
}) |
||||
tfAlternative = newTestForks(testGenesis, types.Forks{ |
||||
&types.Fork{Epoch: 0, Version: []byte{0}}, |
||||
&types.Fork{Epoch: 0x700, Version: []byte{1}}, |
||||
}) |
||||
tfAnotherGenesis = newTestForks(testGenesis2, types.Forks{ |
||||
&types.Fork{Epoch: 0, Version: []byte{0}}, |
||||
}) |
||||
|
||||
tcBase = newTestCommitteeChain(nil, tfBase, true, 0, 10, 400, false) |
||||
tcBaseWithInvalidUpdates = newTestCommitteeChain(tcBase, tfBase, false, 5, 10, 200, false) // signer count too low
|
||||
tcBaseWithBetterUpdates = newTestCommitteeChain(tcBase, tfBase, false, 5, 10, 440, false) |
||||
tcReorgWithWorseUpdates = newTestCommitteeChain(tcBase, tfBase, true, 5, 10, 400, false) |
||||
tcReorgWithWorseUpdates2 = newTestCommitteeChain(tcBase, tfBase, true, 5, 10, 380, false) |
||||
tcReorgWithBetterUpdates = newTestCommitteeChain(tcBase, tfBase, true, 5, 10, 420, false) |
||||
tcReorgWithFinalizedUpdates = newTestCommitteeChain(tcBase, tfBase, true, 5, 10, 400, true) |
||||
tcFork = newTestCommitteeChain(tcBase, tfAlternative, true, 7, 10, 400, false) |
||||
tcAnotherGenesis = newTestCommitteeChain(nil, tfAnotherGenesis, true, 0, 10, 400, false) |
||||
) |
||||
|
||||
func TestCommitteeChainFixedCommitteeRoots(t *testing.T) { |
||||
for _, reload := range []bool{false, true} { |
||||
c := newCommitteeChainTest(t, tfBase, 300, true) |
||||
c.setClockPeriod(7) |
||||
c.addFixedCommitteeRoot(tcBase, 4, nil) |
||||
c.addFixedCommitteeRoot(tcBase, 5, nil) |
||||
c.addFixedCommitteeRoot(tcBase, 6, nil) |
||||
c.addFixedCommitteeRoot(tcBase, 8, ErrInvalidPeriod) // range has to be continuous
|
||||
c.addFixedCommitteeRoot(tcBase, 3, nil) |
||||
c.addFixedCommitteeRoot(tcBase, 2, nil) |
||||
if reload { |
||||
c.reloadChain() |
||||
} |
||||
c.addCommittee(tcBase, 4, nil) |
||||
c.addCommittee(tcBase, 6, ErrInvalidPeriod) // range has to be continuous
|
||||
c.addCommittee(tcBase, 5, nil) |
||||
c.addCommittee(tcBase, 6, nil) |
||||
c.addCommittee(tcAnotherGenesis, 3, ErrWrongCommitteeRoot) |
||||
c.addCommittee(tcBase, 3, nil) |
||||
if reload { |
||||
c.reloadChain() |
||||
} |
||||
c.verifyRange(tcBase, 3, 6) |
||||
} |
||||
} |
||||
|
||||
func TestCommitteeChainCheckpointSync(t *testing.T) { |
||||
for _, enforceTime := range []bool{false, true} { |
||||
for _, reload := range []bool{false, true} { |
||||
c := newCommitteeChainTest(t, tfBase, 300, enforceTime) |
||||
if enforceTime { |
||||
c.setClockPeriod(6) |
||||
} |
||||
c.insertUpdate(tcBase, 3, true, ErrInvalidPeriod) |
||||
c.addFixedCommitteeRoot(tcBase, 3, nil) |
||||
c.addFixedCommitteeRoot(tcBase, 4, nil) |
||||
c.insertUpdate(tcBase, 4, true, ErrInvalidPeriod) // still no committee
|
||||
c.addCommittee(tcBase, 3, nil) |
||||
c.addCommittee(tcBase, 4, nil) |
||||
if reload { |
||||
c.reloadChain() |
||||
} |
||||
c.verifyRange(tcBase, 3, 4) |
||||
c.insertUpdate(tcBase, 3, false, nil) // update can be added without committee here
|
||||
c.insertUpdate(tcBase, 4, false, ErrNeedCommittee) // but not here as committee 5 is not there yet
|
||||
c.insertUpdate(tcBase, 4, true, nil) |
||||
c.verifyRange(tcBase, 3, 5) |
||||
c.insertUpdate(tcBaseWithInvalidUpdates, 5, true, ErrInvalidUpdate) // signer count too low
|
||||
c.insertUpdate(tcBase, 5, true, nil) |
||||
if reload { |
||||
c.reloadChain() |
||||
} |
||||
if enforceTime { |
||||
c.insertUpdate(tcBase, 6, true, ErrInvalidUpdate) // future update rejected
|
||||
c.setClockPeriod(7) |
||||
} |
||||
c.insertUpdate(tcBase, 6, true, nil) // when the time comes it's accepted
|
||||
if reload { |
||||
c.reloadChain() |
||||
} |
||||
if enforceTime { |
||||
c.verifyRange(tcBase, 3, 6) // committee 7 is there but still in the future
|
||||
c.setClockPeriod(8) |
||||
} |
||||
c.verifyRange(tcBase, 3, 7) // now period 7 can also be verified
|
||||
// try reverse syncing an update
|
||||
c.insertUpdate(tcBase, 2, false, ErrInvalidPeriod) // fixed committee is needed first
|
||||
c.addFixedCommitteeRoot(tcBase, 2, nil) |
||||
c.addCommittee(tcBase, 2, nil) |
||||
c.insertUpdate(tcBase, 2, false, nil) |
||||
c.verifyRange(tcBase, 2, 7) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestCommitteeChainReorg(t *testing.T) { |
||||
for _, reload := range []bool{false, true} { |
||||
for _, addBetterUpdates := range []bool{false, true} { |
||||
c := newCommitteeChainTest(t, tfBase, 300, true) |
||||
c.setClockPeriod(11) |
||||
c.addFixedCommitteeRoot(tcBase, 3, nil) |
||||
c.addFixedCommitteeRoot(tcBase, 4, nil) |
||||
c.addCommittee(tcBase, 3, nil) |
||||
for period := uint64(3); period < 10; period++ { |
||||
c.insertUpdate(tcBase, period, true, nil) |
||||
} |
||||
if reload { |
||||
c.reloadChain() |
||||
} |
||||
c.verifyRange(tcBase, 3, 10) |
||||
c.insertUpdate(tcReorgWithWorseUpdates, 5, true, ErrCannotReorg) |
||||
c.insertUpdate(tcReorgWithWorseUpdates2, 5, true, ErrCannotReorg) |
||||
if addBetterUpdates { |
||||
// add better updates for the base chain and expect first reorg to fail
|
||||
// (only add updates as committees should be the same)
|
||||
for period := uint64(5); period < 10; period++ { |
||||
c.insertUpdate(tcBaseWithBetterUpdates, period, false, nil) |
||||
} |
||||
if reload { |
||||
c.reloadChain() |
||||
} |
||||
c.verifyRange(tcBase, 3, 10) // still on the same chain
|
||||
c.insertUpdate(tcReorgWithBetterUpdates, 5, true, ErrCannotReorg) |
||||
} else { |
||||
// reorg with better updates
|
||||
c.insertUpdate(tcReorgWithBetterUpdates, 5, false, ErrNeedCommittee) |
||||
c.verifyRange(tcBase, 3, 10) // no success yet, still on the base chain
|
||||
c.verifyRange(tcReorgWithBetterUpdates, 3, 5) |
||||
c.insertUpdate(tcReorgWithBetterUpdates, 5, true, nil) |
||||
// successful reorg, base chain should only match before the reorg period
|
||||
if reload { |
||||
c.reloadChain() |
||||
} |
||||
c.verifyRange(tcBase, 3, 5) |
||||
c.verifyRange(tcReorgWithBetterUpdates, 3, 6) |
||||
for period := uint64(6); period < 10; period++ { |
||||
c.insertUpdate(tcReorgWithBetterUpdates, period, true, nil) |
||||
} |
||||
c.verifyRange(tcReorgWithBetterUpdates, 3, 10) |
||||
} |
||||
// reorg with finalized updates; should succeed even if base chain updates
|
||||
// have been improved because a finalized update beats everything else
|
||||
c.insertUpdate(tcReorgWithFinalizedUpdates, 5, false, ErrNeedCommittee) |
||||
c.insertUpdate(tcReorgWithFinalizedUpdates, 5, true, nil) |
||||
if reload { |
||||
c.reloadChain() |
||||
} |
||||
c.verifyRange(tcReorgWithFinalizedUpdates, 3, 6) |
||||
for period := uint64(6); period < 10; period++ { |
||||
c.insertUpdate(tcReorgWithFinalizedUpdates, period, true, nil) |
||||
} |
||||
c.verifyRange(tcReorgWithFinalizedUpdates, 3, 10) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestCommitteeChainFork(t *testing.T) { |
||||
c := newCommitteeChainTest(t, tfAlternative, 300, true) |
||||
c.setClockPeriod(11) |
||||
// trying to sync a chain on an alternative fork with the base chain data
|
||||
c.addFixedCommitteeRoot(tcBase, 0, nil) |
||||
c.addFixedCommitteeRoot(tcBase, 1, nil) |
||||
c.addCommittee(tcBase, 0, nil) |
||||
// shared section should sync without errors
|
||||
for period := uint64(0); period < 7; period++ { |
||||
c.insertUpdate(tcBase, period, true, nil) |
||||
} |
||||
c.insertUpdate(tcBase, 7, true, ErrInvalidUpdate) // wrong fork
|
||||
// committee root #7 is still the same but signatures are already signed with
|
||||
// a different fork id so period 7 should only verify on the alternative fork
|
||||
c.verifyRange(tcBase, 0, 6) |
||||
c.verifyRange(tcFork, 0, 7) |
||||
for period := uint64(7); period < 10; period++ { |
||||
c.insertUpdate(tcFork, period, true, nil) |
||||
} |
||||
c.verifyRange(tcFork, 0, 10) |
||||
// reload the chain while switching to the base fork
|
||||
c.config = tfBase |
||||
c.reloadChain() |
||||
// updates 7..9 should be rolled back now
|
||||
c.verifyRange(tcFork, 0, 6) // again, period 7 only verifies on the right fork
|
||||
c.verifyRange(tcBase, 0, 7) |
||||
c.insertUpdate(tcFork, 7, true, ErrInvalidUpdate) // wrong fork
|
||||
for period := uint64(7); period < 10; period++ { |
||||
c.insertUpdate(tcBase, period, true, nil) |
||||
} |
||||
c.verifyRange(tcBase, 0, 10) |
||||
} |
||||
|
||||
type committeeChainTest struct { |
||||
t *testing.T |
||||
db *memorydb.Database |
||||
clock *mclock.Simulated |
||||
config types.ChainConfig |
||||
signerThreshold int |
||||
enforceTime bool |
||||
chain *CommitteeChain |
||||
} |
||||
|
||||
func newCommitteeChainTest(t *testing.T, config types.ChainConfig, signerThreshold int, enforceTime bool) *committeeChainTest { |
||||
c := &committeeChainTest{ |
||||
t: t, |
||||
db: memorydb.New(), |
||||
clock: &mclock.Simulated{}, |
||||
config: config, |
||||
signerThreshold: signerThreshold, |
||||
enforceTime: enforceTime, |
||||
} |
||||
c.chain = newCommitteeChain(c.db, &config, signerThreshold, enforceTime, dummyVerifier{}, c.clock, func() int64 { return int64(c.clock.Now()) }) |
||||
return c |
||||
} |
||||
|
||||
func (c *committeeChainTest) reloadChain() { |
||||
c.chain = newCommitteeChain(c.db, &c.config, c.signerThreshold, c.enforceTime, dummyVerifier{}, c.clock, func() int64 { return int64(c.clock.Now()) }) |
||||
} |
||||
|
||||
func (c *committeeChainTest) setClockPeriod(period float64) { |
||||
target := mclock.AbsTime(period * float64(time.Second*12*params.SyncPeriodLength)) |
||||
wait := time.Duration(target - c.clock.Now()) |
||||
if wait < 0 { |
||||
c.t.Fatalf("Invalid setClockPeriod") |
||||
} |
||||
c.clock.Run(wait) |
||||
} |
||||
|
||||
func (c *committeeChainTest) addFixedCommitteeRoot(tc *testCommitteeChain, period uint64, expErr error) { |
||||
if err := c.chain.addFixedCommitteeRoot(period, tc.periods[period].committee.Root()); err != expErr { |
||||
c.t.Errorf("Incorrect error output from addFixedCommitteeRoot at period %d (expected %v, got %v)", period, expErr, err) |
||||
} |
||||
} |
||||
|
||||
func (c *committeeChainTest) addCommittee(tc *testCommitteeChain, period uint64, expErr error) { |
||||
if err := c.chain.addCommittee(period, tc.periods[period].committee); err != expErr { |
||||
c.t.Errorf("Incorrect error output from addCommittee at period %d (expected %v, got %v)", period, expErr, err) |
||||
} |
||||
} |
||||
|
||||
func (c *committeeChainTest) insertUpdate(tc *testCommitteeChain, period uint64, addCommittee bool, expErr error) { |
||||
var committee *types.SerializedSyncCommittee |
||||
if addCommittee { |
||||
committee = tc.periods[period+1].committee |
||||
} |
||||
if err := c.chain.InsertUpdate(tc.periods[period].update, committee); err != expErr { |
||||
c.t.Errorf("Incorrect error output from InsertUpdate at period %d (expected %v, got %v)", period, expErr, err) |
||||
} |
||||
} |
||||
|
||||
func (c *committeeChainTest) verifySignedHeader(tc *testCommitteeChain, period float64, expOk bool) { |
||||
slot := uint64(period * float64(params.SyncPeriodLength)) |
||||
signedHead := GenerateTestSignedHeader(types.Header{Slot: slot}, &tc.config, tc.periods[types.SyncPeriod(slot)].committee, slot+1, 400) |
||||
if ok, _, _ := c.chain.VerifySignedHeader(signedHead); ok != expOk { |
||||
c.t.Errorf("Incorrect output from VerifySignedHeader at period %f (expected %v, got %v)", period, expOk, ok) |
||||
} |
||||
} |
||||
|
||||
func (c *committeeChainTest) verifyRange(tc *testCommitteeChain, begin, end uint64) { |
||||
if begin > 0 { |
||||
c.verifySignedHeader(tc, float64(begin)-0.5, false) |
||||
} |
||||
for period := begin; period <= end; period++ { |
||||
c.verifySignedHeader(tc, float64(period)+0.5, true) |
||||
} |
||||
c.verifySignedHeader(tc, float64(end)+1.5, false) |
||||
} |
||||
|
||||
func newTestGenesis() types.ChainConfig { |
||||
var config types.ChainConfig |
||||
rand.Read(config.GenesisValidatorsRoot[:]) |
||||
return config |
||||
} |
||||
|
||||
func newTestForks(config types.ChainConfig, forks types.Forks) types.ChainConfig { |
||||
for _, fork := range forks { |
||||
config.AddFork(fork.Name, fork.Epoch, fork.Version) |
||||
} |
||||
return config |
||||
} |
||||
|
||||
func newTestCommitteeChain(parent *testCommitteeChain, config types.ChainConfig, newCommittees bool, begin, end int, signerCount int, finalizedHeader bool) *testCommitteeChain { |
||||
tc := &testCommitteeChain{ |
||||
config: config, |
||||
} |
||||
if parent != nil { |
||||
tc.periods = make([]testPeriod, len(parent.periods)) |
||||
copy(tc.periods, parent.periods) |
||||
} |
||||
if newCommittees { |
||||
if begin == 0 { |
||||
tc.fillCommittees(begin, end+1) |
||||
} else { |
||||
tc.fillCommittees(begin+1, end+1) |
||||
} |
||||
} |
||||
tc.fillUpdates(begin, end, signerCount, finalizedHeader) |
||||
return tc |
||||
} |
||||
|
||||
type testPeriod struct { |
||||
committee *types.SerializedSyncCommittee |
||||
update *types.LightClientUpdate |
||||
} |
||||
|
||||
type testCommitteeChain struct { |
||||
periods []testPeriod |
||||
config types.ChainConfig |
||||
} |
||||
|
||||
func (tc *testCommitteeChain) fillCommittees(begin, end int) { |
||||
if len(tc.periods) <= end { |
||||
tc.periods = append(tc.periods, make([]testPeriod, end+1-len(tc.periods))...) |
||||
} |
||||
for i := begin; i <= end; i++ { |
||||
tc.periods[i].committee = GenerateTestCommittee() |
||||
} |
||||
} |
||||
|
||||
func (tc *testCommitteeChain) fillUpdates(begin, end int, signerCount int, finalizedHeader bool) { |
||||
for i := begin; i <= end; i++ { |
||||
tc.periods[i].update = GenerateTestUpdate(&tc.config, uint64(i), tc.periods[i].committee, tc.periods[i+1].committee, signerCount, finalizedHeader) |
||||
} |
||||
} |
@ -0,0 +1,78 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package light |
||||
|
||||
// periodRange represents a (possibly zero-length) range of integers (sync periods).
|
||||
type periodRange struct { |
||||
Start, End uint64 |
||||
} |
||||
|
||||
// isEmpty returns true if the length of the range is zero.
|
||||
func (a periodRange) isEmpty() bool { |
||||
return a.End == a.Start |
||||
} |
||||
|
||||
// contains returns true if the range includes the given period.
|
||||
func (a periodRange) contains(period uint64) bool { |
||||
return period >= a.Start && period < a.End |
||||
} |
||||
|
||||
// canExpand returns true if the range includes or can be expanded with the given
|
||||
// period (either the range is empty or the given period is inside, right before or
|
||||
// right after the range).
|
||||
func (a periodRange) canExpand(period uint64) bool { |
||||
return a.isEmpty() || (period+1 >= a.Start && period <= a.End) |
||||
} |
||||
|
||||
// expand expands the range with the given period.
|
||||
// This method assumes that canExpand returned true: otherwise this is a no-op.
|
||||
func (a *periodRange) expand(period uint64) { |
||||
if a.isEmpty() { |
||||
a.Start, a.End = period, period+1 |
||||
return |
||||
} |
||||
if a.Start == period+1 { |
||||
a.Start-- |
||||
} |
||||
if a.End == period { |
||||
a.End++ |
||||
} |
||||
} |
||||
|
||||
// split splits the range into two ranges. The 'fromPeriod' will be the first
|
||||
// element in the second range (if present).
|
||||
// The original range is unchanged by this operation
|
||||
func (a *periodRange) split(fromPeriod uint64) (periodRange, periodRange) { |
||||
if fromPeriod <= a.Start { |
||||
// First range empty, everything in second range,
|
||||
return periodRange{}, *a |
||||
} |
||||
if fromPeriod >= a.End { |
||||
// Second range empty, everything in first range,
|
||||
return *a, periodRange{} |
||||
} |
||||
x := periodRange{a.Start, fromPeriod} |
||||
y := periodRange{fromPeriod, a.End} |
||||
return x, y |
||||
} |
||||
|
||||
// each invokes the supplied function fn once per period in range
|
||||
func (a *periodRange) each(fn func(uint64)) { |
||||
for p := a.Start; p < a.End; p++ { |
||||
fn(p) |
||||
} |
||||
} |
@ -0,0 +1,152 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package light |
||||
|
||||
import ( |
||||
"crypto/rand" |
||||
"crypto/sha256" |
||||
mrand "math/rand" |
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/merkle" |
||||
"github.com/ethereum/go-ethereum/beacon/params" |
||||
"github.com/ethereum/go-ethereum/beacon/types" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
func GenerateTestCommittee() *types.SerializedSyncCommittee { |
||||
s := new(types.SerializedSyncCommittee) |
||||
rand.Read(s[:32]) |
||||
return s |
||||
} |
||||
|
||||
func GenerateTestUpdate(config *types.ChainConfig, period uint64, committee, nextCommittee *types.SerializedSyncCommittee, signerCount int, finalizedHeader bool) *types.LightClientUpdate { |
||||
update := new(types.LightClientUpdate) |
||||
update.NextSyncCommitteeRoot = nextCommittee.Root() |
||||
var attestedHeader types.Header |
||||
if finalizedHeader { |
||||
update.FinalizedHeader = new(types.Header) |
||||
*update.FinalizedHeader, update.NextSyncCommitteeBranch = makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+100, params.StateIndexNextSyncCommittee, merkle.Value(update.NextSyncCommitteeRoot)) |
||||
attestedHeader, update.FinalityBranch = makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+200, params.StateIndexFinalBlock, merkle.Value(update.FinalizedHeader.Hash())) |
||||
} else { |
||||
attestedHeader, update.NextSyncCommitteeBranch = makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+2000, params.StateIndexNextSyncCommittee, merkle.Value(update.NextSyncCommitteeRoot)) |
||||
} |
||||
update.AttestedHeader = GenerateTestSignedHeader(attestedHeader, config, committee, attestedHeader.Slot+1, signerCount) |
||||
return update |
||||
} |
||||
|
||||
func GenerateTestSignedHeader(header types.Header, config *types.ChainConfig, committee *types.SerializedSyncCommittee, signatureSlot uint64, signerCount int) types.SignedHeader { |
||||
bitmask := makeBitmask(signerCount) |
||||
signingRoot, _ := config.Forks.SigningRoot(header) |
||||
c, _ := dummyVerifier{}.deserializeSyncCommittee(committee) |
||||
return types.SignedHeader{ |
||||
Header: header, |
||||
Signature: types.SyncAggregate{ |
||||
Signers: bitmask, |
||||
Signature: makeDummySignature(c.(dummySyncCommittee), signingRoot, bitmask), |
||||
}, |
||||
SignatureSlot: signatureSlot, |
||||
} |
||||
} |
||||
|
||||
func GenerateTestCheckpoint(period uint64, committee *types.SerializedSyncCommittee) *types.BootstrapData { |
||||
header, branch := makeTestHeaderWithMerkleProof(types.SyncPeriodStart(period)+200, params.StateIndexSyncCommittee, merkle.Value(committee.Root())) |
||||
return &types.BootstrapData{ |
||||
Header: header, |
||||
Committee: committee, |
||||
CommitteeRoot: committee.Root(), |
||||
CommitteeBranch: branch, |
||||
} |
||||
} |
||||
|
||||
func makeBitmask(signerCount int) (bitmask [params.SyncCommitteeBitmaskSize]byte) { |
||||
for i := 0; i < params.SyncCommitteeSize; i++ { |
||||
if mrand.Intn(params.SyncCommitteeSize-i) < signerCount { |
||||
bitmask[i/8] += byte(1) << (i & 7) |
||||
signerCount-- |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func makeTestHeaderWithMerkleProof(slot, index uint64, value merkle.Value) (types.Header, merkle.Values) { |
||||
var branch merkle.Values |
||||
hasher := sha256.New() |
||||
for index > 1 { |
||||
var proofHash merkle.Value |
||||
rand.Read(proofHash[:]) |
||||
hasher.Reset() |
||||
if index&1 == 0 { |
||||
hasher.Write(value[:]) |
||||
hasher.Write(proofHash[:]) |
||||
} else { |
||||
hasher.Write(proofHash[:]) |
||||
hasher.Write(value[:]) |
||||
} |
||||
hasher.Sum(value[:0]) |
||||
index >>= 1 |
||||
branch = append(branch, proofHash) |
||||
} |
||||
return types.Header{Slot: slot, StateRoot: common.Hash(value)}, branch |
||||
} |
||||
|
||||
// syncCommittee holds either a blsSyncCommittee or a fake dummySyncCommittee used for testing
|
||||
type syncCommittee interface{} |
||||
|
||||
// committeeSigVerifier verifies sync committee signatures (either proper BLS
|
||||
// signatures or fake signatures used for testing)
|
||||
type committeeSigVerifier interface { |
||||
deserializeSyncCommittee(s *types.SerializedSyncCommittee) (syncCommittee, error) |
||||
verifySignature(committee syncCommittee, signedRoot common.Hash, aggregate *types.SyncAggregate) bool |
||||
} |
||||
|
||||
// blsVerifier implements committeeSigVerifier
|
||||
type blsVerifier struct{} |
||||
|
||||
// deserializeSyncCommittee implements committeeSigVerifier
|
||||
func (blsVerifier) deserializeSyncCommittee(s *types.SerializedSyncCommittee) (syncCommittee, error) { |
||||
return s.Deserialize() |
||||
} |
||||
|
||||
// verifySignature implements committeeSigVerifier
|
||||
func (blsVerifier) verifySignature(committee syncCommittee, signingRoot common.Hash, aggregate *types.SyncAggregate) bool { |
||||
return committee.(*types.SyncCommittee).VerifySignature(signingRoot, aggregate) |
||||
} |
||||
|
||||
type dummySyncCommittee [32]byte |
||||
|
||||
// dummyVerifier implements committeeSigVerifier
|
||||
type dummyVerifier struct{} |
||||
|
||||
// deserializeSyncCommittee implements committeeSigVerifier
|
||||
func (dummyVerifier) deserializeSyncCommittee(s *types.SerializedSyncCommittee) (syncCommittee, error) { |
||||
var sc dummySyncCommittee |
||||
copy(sc[:], s[:32]) |
||||
return sc, nil |
||||
} |
||||
|
||||
// verifySignature implements committeeSigVerifier
|
||||
func (dummyVerifier) verifySignature(committee syncCommittee, signingRoot common.Hash, aggregate *types.SyncAggregate) bool { |
||||
return aggregate.Signature == makeDummySignature(committee.(dummySyncCommittee), signingRoot, aggregate.Signers) |
||||
} |
||||
|
||||
func makeDummySignature(committee dummySyncCommittee, signingRoot common.Hash, bitmask [params.SyncCommitteeBitmaskSize]byte) (sig [params.BLSSignatureSize]byte) { |
||||
for i, b := range committee[:] { |
||||
sig[i] = b ^ signingRoot[i] |
||||
} |
||||
copy(sig[32:], bitmask[:]) |
||||
return |
||||
} |
@ -0,0 +1,81 @@ |
||||
// Copyright 2020 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package t8ntool |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"io" |
||||
"math/big" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/vm" |
||||
"github.com/ethereum/go-ethereum/eth/tracers" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
// traceWriter is an vm.EVMLogger which also holds an inner logger/tracer.
|
||||
// When the TxEnd event happens, the inner tracer result is written to the file, and
|
||||
// the file is closed.
|
||||
type traceWriter struct { |
||||
inner vm.EVMLogger |
||||
f io.WriteCloser |
||||
} |
||||
|
||||
// Compile-time interface check
|
||||
var _ = vm.EVMLogger((*traceWriter)(nil)) |
||||
|
||||
func (t *traceWriter) CaptureTxEnd(restGas uint64) { |
||||
t.inner.CaptureTxEnd(restGas) |
||||
defer t.f.Close() |
||||
|
||||
if tracer, ok := t.inner.(tracers.Tracer); ok { |
||||
result, err := tracer.GetResult() |
||||
if err != nil { |
||||
log.Warn("Error in tracer", "err", err) |
||||
return |
||||
} |
||||
err = json.NewEncoder(t.f).Encode(result) |
||||
if err != nil { |
||||
log.Warn("Error writing tracer output", "err", err) |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (t *traceWriter) CaptureTxStart(gasLimit uint64) { t.inner.CaptureTxStart(gasLimit) } |
||||
func (t *traceWriter) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { |
||||
t.inner.CaptureStart(env, from, to, create, input, gas, value) |
||||
} |
||||
|
||||
func (t *traceWriter) CaptureEnd(output []byte, gasUsed uint64, err error) { |
||||
t.inner.CaptureEnd(output, gasUsed, err) |
||||
} |
||||
|
||||
func (t *traceWriter) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { |
||||
t.inner.CaptureEnter(typ, from, to, input, gas, value) |
||||
} |
||||
|
||||
func (t *traceWriter) CaptureExit(output []byte, gasUsed uint64, err error) { |
||||
t.inner.CaptureExit(output, gasUsed, err) |
||||
} |
||||
|
||||
func (t *traceWriter) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { |
||||
t.inner.CaptureState(pc, op, gas, cost, scope, rData, depth, err) |
||||
} |
||||
func (t *traceWriter) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { |
||||
t.inner.CaptureFault(pc, op, gas, cost, scope, depth, err) |
||||
} |
@ -1,52 +0,0 @@ |
||||
# Faucet |
||||
|
||||
The `faucet` is a simplistic web application with the goal of distributing small amounts of Ether in private and test networks. |
||||
|
||||
Users need to post their Ethereum addresses to fund in a Twitter status update or public Facebook post and share the link to the faucet. The faucet will in turn deduplicate user requests and send the Ether. After a funding round, the faucet prevents the same user from requesting again for a pre-configured amount of time, proportional to the amount of Ether requested. |
||||
|
||||
## Operation |
||||
|
||||
The `faucet` is a single binary app (everything included) with all configurations set via command line flags and a few files. |
||||
|
||||
First things first, the `faucet` needs to connect to an Ethereum network, for which it needs the necessary genesis and network infos. Each of the following flags must be set: |
||||
|
||||
- `-genesis` is a path to a file containing the network `genesis.json`. or using: |
||||
- `-goerli` with the faucet with Görli network config |
||||
- `-sepolia` with the faucet with Sepolia network config |
||||
- `-network` is the devp2p network id used during connection |
||||
- `-bootnodes` is a list of `enode://` ids to join the network through |
||||
|
||||
The `faucet` will use the `les` protocol to join the configured Ethereum network and will store its data in `$HOME/.faucet` (currently not configurable). |
||||
|
||||
## Funding |
||||
|
||||
To be able to distribute funds, the `faucet` needs access to an already funded Ethereum account. This can be configured via: |
||||
|
||||
- `-account.json` is a path to the Ethereum account's JSON key file |
||||
- `-account.pass` is a path to a text file with the decryption passphrase |
||||
|
||||
The faucet is able to distribute various amounts of Ether in exchange for various timeouts. These can be configured via: |
||||
|
||||
- `-faucet.amount` is the number of Ethers to send by default |
||||
- `-faucet.minutes` is the time to wait before allowing a rerequest |
||||
- `-faucet.tiers` is the funding tiers to support (x3 time, x2.5 funds) |
||||
|
||||
## Sybil protection |
||||
|
||||
To prevent the same user from exhausting funds in a loop, the `faucet` ties requests to social networks and captcha resolvers. |
||||
|
||||
Captcha protection uses Google's invisible ReCaptcha, thus the `faucet` needs to run on a live domain. The domain needs to be registered in Google's systems to retrieve the captcha API token and secrets. After doing so, captcha protection may be enabled via: |
||||
|
||||
- `-captcha.token` is the API token for ReCaptcha |
||||
- `-captcha.secret` is the API secret for ReCaptcha |
||||
|
||||
Sybil protection via Twitter requires an API key as of 15th December, 2020. To obtain it, a Twitter user must be upgraded to developer status and a new Twitter App deployed with it. The app's `Bearer` token is required by the faucet to retrieve tweet data: |
||||
|
||||
- `-twitter.token` is the Bearer token for `v2` API access |
||||
- `-twitter.token.v1` is the Bearer token for `v1` API access |
||||
|
||||
Sybil protection via Facebook uses the website to directly download post data thus does not currently require an API configuration. |
||||
|
||||
## Miscellaneous |
||||
|
||||
Beside the above - mostly essential - CLI flags, there are a number that can be used to fine-tune the `faucet`'s operation. Please see `faucet --help` for a full list. |
@ -1,891 +0,0 @@ |
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// faucet is an Ether faucet backed by a light client.
|
||||
package main |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
_ "embed" |
||||
"encoding/json" |
||||
"errors" |
||||
"flag" |
||||
"fmt" |
||||
"html/template" |
||||
"io" |
||||
"math" |
||||
"math/big" |
||||
"net/http" |
||||
"net/url" |
||||
"os" |
||||
"path/filepath" |
||||
"regexp" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/accounts/keystore" |
||||
"github.com/ethereum/go-ethereum/cmd/utils" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/eth/downloader" |
||||
"github.com/ethereum/go-ethereum/eth/ethconfig" |
||||
"github.com/ethereum/go-ethereum/ethclient" |
||||
"github.com/ethereum/go-ethereum/ethstats" |
||||
"github.com/ethereum/go-ethereum/internal/version" |
||||
"github.com/ethereum/go-ethereum/les" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/node" |
||||
"github.com/ethereum/go-ethereum/p2p" |
||||
"github.com/ethereum/go-ethereum/p2p/enode" |
||||
"github.com/ethereum/go-ethereum/p2p/nat" |
||||
"github.com/ethereum/go-ethereum/params" |
||||
"github.com/gorilla/websocket" |
||||
) |
||||
|
||||
var ( |
||||
genesisFlag = flag.String("genesis", "", "Genesis json file to seed the chain with") |
||||
apiPortFlag = flag.Int("apiport", 8080, "Listener port for the HTTP API connection") |
||||
ethPortFlag = flag.Int("ethport", 30303, "Listener port for the devp2p connection") |
||||
bootFlag = flag.String("bootnodes", "", "Comma separated bootnode enode URLs to seed with") |
||||
netFlag = flag.Uint64("network", 0, "Network ID to use for the Ethereum protocol") |
||||
statsFlag = flag.String("ethstats", "", "Ethstats network monitoring auth string") |
||||
|
||||
netnameFlag = flag.String("faucet.name", "", "Network name to assign to the faucet") |
||||
payoutFlag = flag.Int("faucet.amount", 1, "Number of Ethers to pay out per user request") |
||||
minutesFlag = flag.Int("faucet.minutes", 1440, "Number of minutes to wait between funding rounds") |
||||
tiersFlag = flag.Int("faucet.tiers", 3, "Number of funding tiers to enable (x3 time, x2.5 funds)") |
||||
|
||||
accJSONFlag = flag.String("account.json", "", "Key json file to fund user requests with") |
||||
accPassFlag = flag.String("account.pass", "", "Decryption password to access faucet funds") |
||||
|
||||
captchaToken = flag.String("captcha.token", "", "Recaptcha site key to authenticate client side") |
||||
captchaSecret = flag.String("captcha.secret", "", "Recaptcha secret key to authenticate server side") |
||||
|
||||
noauthFlag = flag.Bool("noauth", false, "Enables funding requests without authentication") |
||||
logFlag = flag.Int("loglevel", 3, "Log level to use for Ethereum and the faucet") |
||||
|
||||
twitterTokenFlag = flag.String("twitter.token", "", "Bearer token to authenticate with the v2 Twitter API") |
||||
twitterTokenV1Flag = flag.String("twitter.token.v1", "", "Bearer token to authenticate with the v1.1 Twitter API") |
||||
|
||||
goerliFlag = flag.Bool("goerli", false, "Initializes the faucet with Görli network config") |
||||
sepoliaFlag = flag.Bool("sepolia", false, "Initializes the faucet with Sepolia network config") |
||||
) |
||||
|
||||
var ( |
||||
ether = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) |
||||
) |
||||
|
||||
//go:embed faucet.html
|
||||
var websiteTmpl string |
||||
|
||||
func main() { |
||||
// Parse the flags and set up the logger to print everything requested
|
||||
flag.Parse() |
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*logFlag), log.StreamHandler(os.Stderr, log.TerminalFormat(true)))) |
||||
|
||||
// Construct the payout tiers
|
||||
amounts := make([]string, *tiersFlag) |
||||
periods := make([]string, *tiersFlag) |
||||
for i := 0; i < *tiersFlag; i++ { |
||||
// Calculate the amount for the next tier and format it
|
||||
amount := float64(*payoutFlag) * math.Pow(2.5, float64(i)) |
||||
amounts[i] = fmt.Sprintf("%s Ethers", strconv.FormatFloat(amount, 'f', -1, 64)) |
||||
if amount == 1 { |
||||
amounts[i] = strings.TrimSuffix(amounts[i], "s") |
||||
} |
||||
// Calculate the period for the next tier and format it
|
||||
period := *minutesFlag * int(math.Pow(3, float64(i))) |
||||
periods[i] = fmt.Sprintf("%d mins", period) |
||||
if period%60 == 0 { |
||||
period /= 60 |
||||
periods[i] = fmt.Sprintf("%d hours", period) |
||||
|
||||
if period%24 == 0 { |
||||
period /= 24 |
||||
periods[i] = fmt.Sprintf("%d days", period) |
||||
} |
||||
} |
||||
if period == 1 { |
||||
periods[i] = strings.TrimSuffix(periods[i], "s") |
||||
} |
||||
} |
||||
website := new(bytes.Buffer) |
||||
err := template.Must(template.New("").Parse(websiteTmpl)).Execute(website, map[string]interface{}{ |
||||
"Network": *netnameFlag, |
||||
"Amounts": amounts, |
||||
"Periods": periods, |
||||
"Recaptcha": *captchaToken, |
||||
"NoAuth": *noauthFlag, |
||||
}) |
||||
if err != nil { |
||||
log.Crit("Failed to render the faucet template", "err", err) |
||||
} |
||||
// Load and parse the genesis block requested by the user
|
||||
genesis, err := getGenesis(*genesisFlag, *goerliFlag, *sepoliaFlag) |
||||
if err != nil { |
||||
log.Crit("Failed to parse genesis config", "err", err) |
||||
} |
||||
// Convert the bootnodes to internal enode representations
|
||||
var enodes []*enode.Node |
||||
for _, boot := range strings.Split(*bootFlag, ",") { |
||||
if url, err := enode.Parse(enode.ValidSchemes, boot); err == nil { |
||||
enodes = append(enodes, url) |
||||
} else { |
||||
log.Error("Failed to parse bootnode URL", "url", boot, "err", err) |
||||
} |
||||
} |
||||
// Load up the account key and decrypt its password
|
||||
blob, err := os.ReadFile(*accPassFlag) |
||||
if err != nil { |
||||
log.Crit("Failed to read account password contents", "file", *accPassFlag, "err", err) |
||||
} |
||||
pass := strings.TrimSuffix(string(blob), "\n") |
||||
|
||||
ks := keystore.NewKeyStore(filepath.Join(os.Getenv("HOME"), ".faucet", "keys"), keystore.StandardScryptN, keystore.StandardScryptP) |
||||
if blob, err = os.ReadFile(*accJSONFlag); err != nil { |
||||
log.Crit("Failed to read account key contents", "file", *accJSONFlag, "err", err) |
||||
} |
||||
acc, err := ks.Import(blob, pass, pass) |
||||
if err != nil && err != keystore.ErrAccountAlreadyExists { |
||||
log.Crit("Failed to import faucet signer account", "err", err) |
||||
} |
||||
if err := ks.Unlock(acc, pass); err != nil { |
||||
log.Crit("Failed to unlock faucet signer account", "err", err) |
||||
} |
||||
// Assemble and start the faucet light service
|
||||
faucet, err := newFaucet(genesis, *ethPortFlag, enodes, *netFlag, *statsFlag, ks, website.Bytes()) |
||||
if err != nil { |
||||
log.Crit("Failed to start faucet", "err", err) |
||||
} |
||||
defer faucet.close() |
||||
|
||||
if err := faucet.listenAndServe(*apiPortFlag); err != nil { |
||||
log.Crit("Failed to launch faucet API", "err", err) |
||||
} |
||||
} |
||||
|
||||
// request represents an accepted funding request.
|
||||
type request struct { |
||||
Avatar string `json:"avatar"` // Avatar URL to make the UI nicer
|
||||
Account common.Address `json:"account"` // Ethereum address being funded
|
||||
Time time.Time `json:"time"` // Timestamp when the request was accepted
|
||||
Tx *types.Transaction `json:"tx"` // Transaction funding the account
|
||||
} |
||||
|
||||
// faucet represents a crypto faucet backed by an Ethereum light client.
|
||||
type faucet struct { |
||||
config *params.ChainConfig // Chain configurations for signing
|
||||
stack *node.Node // Ethereum protocol stack
|
||||
client *ethclient.Client // Client connection to the Ethereum chain
|
||||
index []byte // Index page to serve up on the web
|
||||
|
||||
keystore *keystore.KeyStore // Keystore containing the single signer
|
||||
account accounts.Account // Account funding user faucet requests
|
||||
head *types.Header // Current head header of the faucet
|
||||
balance *big.Int // Current balance of the faucet
|
||||
nonce uint64 // Current pending nonce of the faucet
|
||||
price *big.Int // Current gas price to issue funds with
|
||||
|
||||
conns []*wsConn // Currently live websocket connections
|
||||
timeouts map[string]time.Time // History of users and their funding timeouts
|
||||
reqs []*request // Currently pending funding requests
|
||||
update chan struct{} // Channel to signal request updates
|
||||
|
||||
lock sync.RWMutex // Lock protecting the faucet's internals
|
||||
} |
||||
|
||||
// wsConn wraps a websocket connection with a write mutex as the underlying
|
||||
// websocket library does not synchronize access to the stream.
|
||||
type wsConn struct { |
||||
conn *websocket.Conn |
||||
wlock sync.Mutex |
||||
} |
||||
|
||||
func newFaucet(genesis *core.Genesis, port int, enodes []*enode.Node, network uint64, stats string, ks *keystore.KeyStore, index []byte) (*faucet, error) { |
||||
// Assemble the raw devp2p protocol stack
|
||||
git, _ := version.VCS() |
||||
stack, err := node.New(&node.Config{ |
||||
Name: "geth", |
||||
Version: params.VersionWithCommit(git.Commit, git.Date), |
||||
DataDir: filepath.Join(os.Getenv("HOME"), ".faucet"), |
||||
P2P: p2p.Config{ |
||||
NAT: nat.Any(), |
||||
NoDiscovery: true, |
||||
DiscoveryV5: true, |
||||
ListenAddr: fmt.Sprintf(":%d", port), |
||||
MaxPeers: 25, |
||||
BootstrapNodesV5: enodes, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// Assemble the Ethereum light client protocol
|
||||
cfg := ethconfig.Defaults |
||||
cfg.SyncMode = downloader.LightSync |
||||
cfg.NetworkId = network |
||||
cfg.Genesis = genesis |
||||
utils.SetDNSDiscoveryDefaults(&cfg, genesis.ToBlock().Hash()) |
||||
|
||||
lesBackend, err := les.New(stack, &cfg) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("Failed to register the Ethereum service: %w", err) |
||||
} |
||||
|
||||
// Assemble the ethstats monitoring and reporting service'
|
||||
if stats != "" { |
||||
if err := ethstats.New(stack, lesBackend.ApiBackend, lesBackend.Engine(), stats); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
// Boot up the client and ensure it connects to bootnodes
|
||||
if err := stack.Start(); err != nil { |
||||
return nil, err |
||||
} |
||||
for _, boot := range enodes { |
||||
old, err := enode.Parse(enode.ValidSchemes, boot.String()) |
||||
if err == nil { |
||||
stack.Server().AddPeer(old) |
||||
} |
||||
} |
||||
// Attach to the client and retrieve and interesting metadatas
|
||||
api := stack.Attach() |
||||
client := ethclient.NewClient(api) |
||||
|
||||
return &faucet{ |
||||
config: genesis.Config, |
||||
stack: stack, |
||||
client: client, |
||||
index: index, |
||||
keystore: ks, |
||||
account: ks.Accounts()[0], |
||||
timeouts: make(map[string]time.Time), |
||||
update: make(chan struct{}, 1), |
||||
}, nil |
||||
} |
||||
|
||||
// close terminates the Ethereum connection and tears down the faucet.
|
||||
func (f *faucet) close() error { |
||||
return f.stack.Close() |
||||
} |
||||
|
||||
// listenAndServe registers the HTTP handlers for the faucet and boots it up
|
||||
// for service user funding requests.
|
||||
func (f *faucet) listenAndServe(port int) error { |
||||
go f.loop() |
||||
|
||||
http.HandleFunc("/", f.webHandler) |
||||
http.HandleFunc("/api", f.apiHandler) |
||||
return http.ListenAndServe(fmt.Sprintf(":%d", port), nil) |
||||
} |
||||
|
||||
// webHandler handles all non-api requests, simply flattening and returning the
|
||||
// faucet website.
|
||||
func (f *faucet) webHandler(w http.ResponseWriter, r *http.Request) { |
||||
w.Write(f.index) |
||||
} |
||||
|
||||
// apiHandler handles requests for Ether grants and transaction statuses.
|
||||
func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) { |
||||
upgrader := websocket.Upgrader{} |
||||
conn, err := upgrader.Upgrade(w, r, nil) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
// Start tracking the connection and drop at the end
|
||||
defer conn.Close() |
||||
|
||||
f.lock.Lock() |
||||
wsconn := &wsConn{conn: conn} |
||||
f.conns = append(f.conns, wsconn) |
||||
f.lock.Unlock() |
||||
|
||||
defer func() { |
||||
f.lock.Lock() |
||||
for i, c := range f.conns { |
||||
if c.conn == conn { |
||||
f.conns = append(f.conns[:i], f.conns[i+1:]...) |
||||
break |
||||
} |
||||
} |
||||
f.lock.Unlock() |
||||
}() |
||||
// Gather the initial stats from the network to report
|
||||
var ( |
||||
head *types.Header |
||||
balance *big.Int |
||||
nonce uint64 |
||||
) |
||||
for head == nil || balance == nil { |
||||
// Retrieve the current stats cached by the faucet
|
||||
f.lock.RLock() |
||||
if f.head != nil { |
||||
head = types.CopyHeader(f.head) |
||||
} |
||||
if f.balance != nil { |
||||
balance = new(big.Int).Set(f.balance) |
||||
} |
||||
nonce = f.nonce |
||||
f.lock.RUnlock() |
||||
|
||||
if head == nil || balance == nil { |
||||
// Report the faucet offline until initial stats are ready
|
||||
//lint:ignore ST1005 This error is to be displayed in the browser
|
||||
if err = sendError(wsconn, errors.New("Faucet offline")); err != nil { |
||||
log.Warn("Failed to send faucet error to client", "err", err) |
||||
return |
||||
} |
||||
time.Sleep(3 * time.Second) |
||||
} |
||||
} |
||||
// Send over the initial stats and the latest header
|
||||
f.lock.RLock() |
||||
reqs := f.reqs |
||||
f.lock.RUnlock() |
||||
if err = send(wsconn, map[string]interface{}{ |
||||
"funds": new(big.Int).Div(balance, ether), |
||||
"funded": nonce, |
||||
"peers": f.stack.Server().PeerCount(), |
||||
"requests": reqs, |
||||
}, 3*time.Second); err != nil { |
||||
log.Warn("Failed to send initial stats to client", "err", err) |
||||
return |
||||
} |
||||
if err = send(wsconn, head, 3*time.Second); err != nil { |
||||
log.Warn("Failed to send initial header to client", "err", err) |
||||
return |
||||
} |
||||
// Keep reading requests from the websocket until the connection breaks
|
||||
for { |
||||
// Fetch the next funding request and validate against github
|
||||
var msg struct { |
||||
URL string `json:"url"` |
||||
Tier uint `json:"tier"` |
||||
Captcha string `json:"captcha"` |
||||
} |
||||
if err = conn.ReadJSON(&msg); err != nil { |
||||
return |
||||
} |
||||
if !*noauthFlag && !strings.HasPrefix(msg.URL, "https://twitter.com/") && !strings.HasPrefix(msg.URL, "https://www.facebook.com/") { |
||||
if err = sendError(wsconn, errors.New("URL doesn't link to supported services")); err != nil { |
||||
log.Warn("Failed to send URL error to client", "err", err) |
||||
return |
||||
} |
||||
continue |
||||
} |
||||
if msg.Tier >= uint(*tiersFlag) { |
||||
//lint:ignore ST1005 This error is to be displayed in the browser
|
||||
if err = sendError(wsconn, errors.New("Invalid funding tier requested")); err != nil { |
||||
log.Warn("Failed to send tier error to client", "err", err) |
||||
return |
||||
} |
||||
continue |
||||
} |
||||
log.Info("Faucet funds requested", "url", msg.URL, "tier", msg.Tier) |
||||
|
||||
// If captcha verifications are enabled, make sure we're not dealing with a robot
|
||||
if *captchaToken != "" { |
||||
form := url.Values{} |
||||
form.Add("secret", *captchaSecret) |
||||
form.Add("response", msg.Captcha) |
||||
|
||||
res, err := http.PostForm("https://www.google.com/recaptcha/api/siteverify", form) |
||||
if err != nil { |
||||
if err = sendError(wsconn, err); err != nil { |
||||
log.Warn("Failed to send captcha post error to client", "err", err) |
||||
return |
||||
} |
||||
continue |
||||
} |
||||
var result struct { |
||||
Success bool `json:"success"` |
||||
Errors json.RawMessage `json:"error-codes"` |
||||
} |
||||
err = json.NewDecoder(res.Body).Decode(&result) |
||||
res.Body.Close() |
||||
if err != nil { |
||||
if err = sendError(wsconn, err); err != nil { |
||||
log.Warn("Failed to send captcha decode error to client", "err", err) |
||||
return |
||||
} |
||||
continue |
||||
} |
||||
if !result.Success { |
||||
log.Warn("Captcha verification failed", "err", string(result.Errors)) |
||||
//lint:ignore ST1005 it's funny and the robot won't mind
|
||||
if err = sendError(wsconn, errors.New("Beep-bop, you're a robot!")); err != nil { |
||||
log.Warn("Failed to send captcha failure to client", "err", err) |
||||
return |
||||
} |
||||
continue |
||||
} |
||||
} |
||||
// Retrieve the Ethereum address to fund, the requesting user and a profile picture
|
||||
var ( |
||||
id string |
||||
username string |
||||
avatar string |
||||
address common.Address |
||||
) |
||||
switch { |
||||
case strings.HasPrefix(msg.URL, "https://twitter.com/"): |
||||
id, username, avatar, address, err = authTwitter(msg.URL, *twitterTokenV1Flag, *twitterTokenFlag) |
||||
case strings.HasPrefix(msg.URL, "https://www.facebook.com/"): |
||||
username, avatar, address, err = authFacebook(msg.URL) |
||||
id = username |
||||
case *noauthFlag: |
||||
username, avatar, address, err = authNoAuth(msg.URL) |
||||
id = username |
||||
default: |
||||
//lint:ignore ST1005 This error is to be displayed in the browser
|
||||
err = errors.New("Something funky happened, please open an issue at https://github.com/ethereum/go-ethereum/issues") |
||||
} |
||||
if err != nil { |
||||
if err = sendError(wsconn, err); err != nil { |
||||
log.Warn("Failed to send prefix error to client", "err", err) |
||||
return |
||||
} |
||||
continue |
||||
} |
||||
log.Info("Faucet request valid", "url", msg.URL, "tier", msg.Tier, "user", username, "address", address) |
||||
|
||||
// Ensure the user didn't request funds too recently
|
||||
f.lock.Lock() |
||||
var ( |
||||
fund bool |
||||
timeout time.Time |
||||
) |
||||
if timeout = f.timeouts[id]; time.Now().After(timeout) { |
||||
// User wasn't funded recently, create the funding transaction
|
||||
amount := new(big.Int).Mul(big.NewInt(int64(*payoutFlag)), ether) |
||||
amount = new(big.Int).Mul(amount, new(big.Int).Exp(big.NewInt(5), big.NewInt(int64(msg.Tier)), nil)) |
||||
amount = new(big.Int).Div(amount, new(big.Int).Exp(big.NewInt(2), big.NewInt(int64(msg.Tier)), nil)) |
||||
|
||||
tx := types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, amount, 21000, f.price, nil) |
||||
signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainID) |
||||
if err != nil { |
||||
f.lock.Unlock() |
||||
if err = sendError(wsconn, err); err != nil { |
||||
log.Warn("Failed to send transaction creation error to client", "err", err) |
||||
return |
||||
} |
||||
continue |
||||
} |
||||
// Submit the transaction and mark as funded if successful
|
||||
if err := f.client.SendTransaction(context.Background(), signed); err != nil { |
||||
f.lock.Unlock() |
||||
if err = sendError(wsconn, err); err != nil { |
||||
log.Warn("Failed to send transaction transmission error to client", "err", err) |
||||
return |
||||
} |
||||
continue |
||||
} |
||||
f.reqs = append(f.reqs, &request{ |
||||
Avatar: avatar, |
||||
Account: address, |
||||
Time: time.Now(), |
||||
Tx: signed, |
||||
}) |
||||
timeout := time.Duration(*minutesFlag*int(math.Pow(3, float64(msg.Tier)))) * time.Minute |
||||
grace := timeout / 288 // 24h timeout => 5m grace
|
||||
|
||||
f.timeouts[id] = time.Now().Add(timeout - grace) |
||||
fund = true |
||||
} |
||||
f.lock.Unlock() |
||||
|
||||
// Send an error if too frequent funding, othewise a success
|
||||
if !fund { |
||||
if err = sendError(wsconn, fmt.Errorf("%s left until next allowance", common.PrettyDuration(time.Until(timeout)))); err != nil { // nolint: gosimple
|
||||
log.Warn("Failed to send funding error to client", "err", err) |
||||
return |
||||
} |
||||
continue |
||||
} |
||||
if err = sendSuccess(wsconn, fmt.Sprintf("Funding request accepted for %s into %s", username, address.Hex())); err != nil { |
||||
log.Warn("Failed to send funding success to client", "err", err) |
||||
return |
||||
} |
||||
select { |
||||
case f.update <- struct{}{}: |
||||
default: |
||||
} |
||||
} |
||||
} |
||||
|
||||
// refresh attempts to retrieve the latest header from the chain and extract the
|
||||
// associated faucet balance and nonce for connectivity caching.
|
||||
func (f *faucet) refresh(head *types.Header) error { |
||||
// Ensure a state update does not run for too long
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
||||
defer cancel() |
||||
|
||||
// If no header was specified, use the current chain head
|
||||
var err error |
||||
if head == nil { |
||||
if head, err = f.client.HeaderByNumber(ctx, nil); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
// Retrieve the balance, nonce and gas price from the current head
|
||||
var ( |
||||
balance *big.Int |
||||
nonce uint64 |
||||
price *big.Int |
||||
) |
||||
if balance, err = f.client.BalanceAt(ctx, f.account.Address, head.Number); err != nil { |
||||
return err |
||||
} |
||||
if nonce, err = f.client.NonceAt(ctx, f.account.Address, head.Number); err != nil { |
||||
return err |
||||
} |
||||
if price, err = f.client.SuggestGasPrice(ctx); err != nil { |
||||
return err |
||||
} |
||||
// Everything succeeded, update the cached stats and eject old requests
|
||||
f.lock.Lock() |
||||
f.head, f.balance = head, balance |
||||
f.price, f.nonce = price, nonce |
||||
for len(f.reqs) > 0 && f.reqs[0].Tx.Nonce() < f.nonce { |
||||
f.reqs = f.reqs[1:] |
||||
} |
||||
f.lock.Unlock() |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// loop keeps waiting for interesting events and pushes them out to connected
|
||||
// websockets.
|
||||
func (f *faucet) loop() { |
||||
// Wait for chain events and push them to clients
|
||||
heads := make(chan *types.Header, 16) |
||||
sub, err := f.client.SubscribeNewHead(context.Background(), heads) |
||||
if err != nil { |
||||
log.Crit("Failed to subscribe to head events", "err", err) |
||||
} |
||||
defer sub.Unsubscribe() |
||||
|
||||
// Start a goroutine to update the state from head notifications in the background
|
||||
update := make(chan *types.Header) |
||||
|
||||
go func() { |
||||
for head := range update { |
||||
// New chain head arrived, query the current stats and stream to clients
|
||||
timestamp := time.Unix(int64(head.Time), 0) |
||||
if time.Since(timestamp) > time.Hour { |
||||
log.Warn("Skipping faucet refresh, head too old", "number", head.Number, "hash", head.Hash(), "age", common.PrettyAge(timestamp)) |
||||
continue |
||||
} |
||||
if err := f.refresh(head); err != nil { |
||||
log.Warn("Failed to update faucet state", "block", head.Number, "hash", head.Hash(), "err", err) |
||||
continue |
||||
} |
||||
// Faucet state retrieved, update locally and send to clients
|
||||
f.lock.RLock() |
||||
log.Info("Updated faucet state", "number", head.Number, "hash", head.Hash(), "age", common.PrettyAge(timestamp), "balance", f.balance, "nonce", f.nonce, "price", f.price) |
||||
|
||||
balance := new(big.Int).Div(f.balance, ether) |
||||
peers := f.stack.Server().PeerCount() |
||||
|
||||
for _, conn := range f.conns { |
||||
if err := send(conn, map[string]interface{}{ |
||||
"funds": balance, |
||||
"funded": f.nonce, |
||||
"peers": peers, |
||||
"requests": f.reqs, |
||||
}, time.Second); err != nil { |
||||
log.Warn("Failed to send stats to client", "err", err) |
||||
conn.conn.Close() |
||||
continue |
||||
} |
||||
if err := send(conn, head, time.Second); err != nil { |
||||
log.Warn("Failed to send header to client", "err", err) |
||||
conn.conn.Close() |
||||
} |
||||
} |
||||
f.lock.RUnlock() |
||||
} |
||||
}() |
||||
// Wait for various events and assing to the appropriate background threads
|
||||
for { |
||||
select { |
||||
case head := <-heads: |
||||
// New head arrived, send if for state update if there's none running
|
||||
select { |
||||
case update <- head: |
||||
default: |
||||
} |
||||
|
||||
case <-f.update: |
||||
// Pending requests updated, stream to clients
|
||||
f.lock.RLock() |
||||
for _, conn := range f.conns { |
||||
if err := send(conn, map[string]interface{}{"requests": f.reqs}, time.Second); err != nil { |
||||
log.Warn("Failed to send requests to client", "err", err) |
||||
conn.conn.Close() |
||||
} |
||||
} |
||||
f.lock.RUnlock() |
||||
} |
||||
} |
||||
} |
||||
|
||||
// sends transmits a data packet to the remote end of the websocket, but also
|
||||
// setting a write deadline to prevent waiting forever on the node.
|
||||
func send(conn *wsConn, value interface{}, timeout time.Duration) error { |
||||
if timeout == 0 { |
||||
timeout = 60 * time.Second |
||||
} |
||||
conn.wlock.Lock() |
||||
defer conn.wlock.Unlock() |
||||
conn.conn.SetWriteDeadline(time.Now().Add(timeout)) |
||||
return conn.conn.WriteJSON(value) |
||||
} |
||||
|
||||
// sendError transmits an error to the remote end of the websocket, also setting
|
||||
// the write deadline to 1 second to prevent waiting forever.
|
||||
func sendError(conn *wsConn, err error) error { |
||||
return send(conn, map[string]string{"error": err.Error()}, time.Second) |
||||
} |
||||
|
||||
// sendSuccess transmits a success message to the remote end of the websocket, also
|
||||
// setting the write deadline to 1 second to prevent waiting forever.
|
||||
func sendSuccess(conn *wsConn, msg string) error { |
||||
return send(conn, map[string]string{"success": msg}, time.Second) |
||||
} |
||||
|
||||
// authTwitter tries to authenticate a faucet request using Twitter posts, returning
|
||||
// the uniqueness identifier (user id/username), username, avatar URL and Ethereum address to fund on success.
|
||||
func authTwitter(url string, tokenV1, tokenV2 string) (string, string, string, common.Address, error) { |
||||
// Ensure the user specified a meaningful URL, no fancy nonsense
|
||||
parts := strings.Split(url, "/") |
||||
if len(parts) < 4 || parts[len(parts)-2] != "status" { |
||||
//lint:ignore ST1005 This error is to be displayed in the browser
|
||||
return "", "", "", common.Address{}, errors.New("Invalid Twitter status URL") |
||||
} |
||||
// Strip any query parameters from the tweet id and ensure it's numeric
|
||||
tweetID := strings.Split(parts[len(parts)-1], "?")[0] |
||||
if !regexp.MustCompile("^[0-9]+$").MatchString(tweetID) { |
||||
return "", "", "", common.Address{}, errors.New("Invalid Tweet URL") |
||||
} |
||||
// Twitter's API isn't really friendly with direct links.
|
||||
// It is restricted to 300 queries / 15 minute with an app api key.
|
||||
// Anything more will require read only authorization from the users and that we want to avoid.
|
||||
|
||||
// If Twitter bearer token is provided, use the API, selecting the version
|
||||
// the user would prefer (currently there's a limit of 1 v2 app / developer
|
||||
// but unlimited v1.1 apps).
|
||||
switch { |
||||
case tokenV1 != "": |
||||
return authTwitterWithTokenV1(tweetID, tokenV1) |
||||
case tokenV2 != "": |
||||
return authTwitterWithTokenV2(tweetID, tokenV2) |
||||
} |
||||
// Twitter API token isn't provided so we just load the public posts
|
||||
// and scrape it for the Ethereum address and profile URL. We need to load
|
||||
// the mobile page though since the main page loads tweet contents via JS.
|
||||
url = strings.Replace(url, "https://twitter.com/", "https://mobile.twitter.com/", 1) |
||||
|
||||
res, err := http.Get(url) |
||||
if err != nil { |
||||
return "", "", "", common.Address{}, err |
||||
} |
||||
defer res.Body.Close() |
||||
|
||||
// Resolve the username from the final redirect, no intermediate junk
|
||||
parts = strings.Split(res.Request.URL.String(), "/") |
||||
if len(parts) < 4 || parts[len(parts)-2] != "status" { |
||||
//lint:ignore ST1005 This error is to be displayed in the browser
|
||||
return "", "", "", common.Address{}, errors.New("Invalid Twitter status URL") |
||||
} |
||||
username := parts[len(parts)-3] |
||||
|
||||
body, err := io.ReadAll(res.Body) |
||||
if err != nil { |
||||
return "", "", "", common.Address{}, err |
||||
} |
||||
address := common.HexToAddress(string(regexp.MustCompile("0x[0-9a-fA-F]{40}").Find(body))) |
||||
if address == (common.Address{}) { |
||||
//lint:ignore ST1005 This error is to be displayed in the browser
|
||||
return "", "", "", common.Address{}, errors.New("No Ethereum address found to fund") |
||||
} |
||||
var avatar string |
||||
if parts = regexp.MustCompile(`src="([^"]+twimg\.com/profile_images[^"]+)"`).FindStringSubmatch(string(body)); len(parts) == 2 { |
||||
avatar = parts[1] |
||||
} |
||||
return username + "@twitter", username, avatar, address, nil |
||||
} |
||||
|
||||
// authTwitterWithTokenV1 tries to authenticate a faucet request using Twitter's v1
|
||||
// API, returning the user id, username, avatar URL and Ethereum address to fund on
|
||||
// success.
|
||||
func authTwitterWithTokenV1(tweetID string, token string) (string, string, string, common.Address, error) { |
||||
// Query the tweet details from Twitter
|
||||
url := fmt.Sprintf("https://api.twitter.com/1.1/statuses/show.json?id=%s", tweetID) |
||||
req, err := http.NewRequest(http.MethodGet, url, nil) |
||||
if err != nil { |
||||
return "", "", "", common.Address{}, err |
||||
} |
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) |
||||
res, err := http.DefaultClient.Do(req) |
||||
if err != nil { |
||||
return "", "", "", common.Address{}, err |
||||
} |
||||
defer res.Body.Close() |
||||
|
||||
var result struct { |
||||
Text string `json:"text"` |
||||
User struct { |
||||
ID string `json:"id_str"` |
||||
Username string `json:"screen_name"` |
||||
Avatar string `json:"profile_image_url"` |
||||
} `json:"user"` |
||||
} |
||||
err = json.NewDecoder(res.Body).Decode(&result) |
||||
if err != nil { |
||||
return "", "", "", common.Address{}, err |
||||
} |
||||
address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(result.Text)) |
||||
if address == (common.Address{}) { |
||||
//lint:ignore ST1005 This error is to be displayed in the browser
|
||||
return "", "", "", common.Address{}, errors.New("No Ethereum address found to fund") |
||||
} |
||||
return result.User.ID + "@twitter", result.User.Username, result.User.Avatar, address, nil |
||||
} |
||||
|
||||
// authTwitterWithTokenV2 tries to authenticate a faucet request using Twitter's v2
|
||||
// API, returning the user id, username, avatar URL and Ethereum address to fund on
|
||||
// success.
|
||||
func authTwitterWithTokenV2(tweetID string, token string) (string, string, string, common.Address, error) { |
||||
// Query the tweet details from Twitter
|
||||
url := fmt.Sprintf("https://api.twitter.com/2/tweets/%s?expansions=author_id&user.fields=profile_image_url", tweetID) |
||||
req, err := http.NewRequest(http.MethodGet, url, nil) |
||||
if err != nil { |
||||
return "", "", "", common.Address{}, err |
||||
} |
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) |
||||
res, err := http.DefaultClient.Do(req) |
||||
if err != nil { |
||||
return "", "", "", common.Address{}, err |
||||
} |
||||
defer res.Body.Close() |
||||
|
||||
var result struct { |
||||
Data struct { |
||||
AuthorID string `json:"author_id"` |
||||
Text string `json:"text"` |
||||
} `json:"data"` |
||||
Includes struct { |
||||
Users []struct { |
||||
ID string `json:"id"` |
||||
Username string `json:"username"` |
||||
Avatar string `json:"profile_image_url"` |
||||
} `json:"users"` |
||||
} `json:"includes"` |
||||
} |
||||
|
||||
err = json.NewDecoder(res.Body).Decode(&result) |
||||
if err != nil { |
||||
return "", "", "", common.Address{}, err |
||||
} |
||||
|
||||
address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(result.Data.Text)) |
||||
if address == (common.Address{}) { |
||||
//lint:ignore ST1005 This error is to be displayed in the browser
|
||||
return "", "", "", common.Address{}, errors.New("No Ethereum address found to fund") |
||||
} |
||||
return result.Data.AuthorID + "@twitter", result.Includes.Users[0].Username, result.Includes.Users[0].Avatar, address, nil |
||||
} |
||||
|
||||
// authFacebook tries to authenticate a faucet request using Facebook posts,
|
||||
// returning the username, avatar URL and Ethereum address to fund on success.
|
||||
func authFacebook(url string) (string, string, common.Address, error) { |
||||
// Ensure the user specified a meaningful URL, no fancy nonsense
|
||||
parts := strings.Split(strings.Split(url, "?")[0], "/") |
||||
if parts[len(parts)-1] == "" { |
||||
parts = parts[0 : len(parts)-1] |
||||
} |
||||
if len(parts) < 4 || parts[len(parts)-2] != "posts" { |
||||
//lint:ignore ST1005 This error is to be displayed in the browser
|
||||
return "", "", common.Address{}, errors.New("Invalid Facebook post URL") |
||||
} |
||||
username := parts[len(parts)-3] |
||||
|
||||
// Facebook's Graph API isn't really friendly with direct links. Still, we don't
|
||||
// want to do ask read permissions from users, so just load the public posts and
|
||||
// scrape it for the Ethereum address and profile URL.
|
||||
//
|
||||
// Facebook recently changed their desktop webpage to use AJAX for loading post
|
||||
// content, so switch over to the mobile site for now. Will probably end up having
|
||||
// to use the API eventually.
|
||||
crawl := strings.Replace(url, "www.facebook.com", "m.facebook.com", 1) |
||||
|
||||
res, err := http.Get(crawl) |
||||
if err != nil { |
||||
return "", "", common.Address{}, err |
||||
} |
||||
defer res.Body.Close() |
||||
|
||||
body, err := io.ReadAll(res.Body) |
||||
if err != nil { |
||||
return "", "", common.Address{}, err |
||||
} |
||||
address := common.HexToAddress(string(regexp.MustCompile("0x[0-9a-fA-F]{40}").Find(body))) |
||||
if address == (common.Address{}) { |
||||
//lint:ignore ST1005 This error is to be displayed in the browser
|
||||
return "", "", common.Address{}, errors.New("No Ethereum address found to fund. Please check the post URL and verify that it can be viewed publicly.") |
||||
} |
||||
var avatar string |
||||
if parts = regexp.MustCompile(`src="([^"]+fbcdn\.net[^"]+)"`).FindStringSubmatch(string(body)); len(parts) == 2 { |
||||
avatar = parts[1] |
||||
} |
||||
return username + "@facebook", avatar, address, nil |
||||
} |
||||
|
||||
// authNoAuth tries to interpret a faucet request as a plain Ethereum address,
|
||||
// without actually performing any remote authentication. This mode is prone to
|
||||
// Byzantine attack, so only ever use for truly private networks.
|
||||
func authNoAuth(url string) (string, string, common.Address, error) { |
||||
address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(url)) |
||||
if address == (common.Address{}) { |
||||
//lint:ignore ST1005 This error is to be displayed in the browser
|
||||
return "", "", common.Address{}, errors.New("No Ethereum address found to fund") |
||||
} |
||||
return address.Hex() + "@noauth", "", address, nil |
||||
} |
||||
|
||||
// getGenesis returns a genesis based on input args
|
||||
func getGenesis(genesisFlag string, goerliFlag bool, sepoliaFlag bool) (*core.Genesis, error) { |
||||
switch { |
||||
case genesisFlag != "": |
||||
var genesis core.Genesis |
||||
err := common.LoadJSON(genesisFlag, &genesis) |
||||
return &genesis, err |
||||
case goerliFlag: |
||||
return core.DefaultGoerliGenesisBlock(), nil |
||||
case sepoliaFlag: |
||||
return core.DefaultSepoliaGenesisBlock(), nil |
||||
default: |
||||
return nil, errors.New("no genesis flag provided") |
||||
} |
||||
} |
@ -1,233 +0,0 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
||||
|
||||
<title>{{.Network}}: Authenticated Faucet</title> |
||||
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" /> |
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" /> |
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> |
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-noty/2.4.1/packaged/jquery.noty.packaged.min.js"></script> |
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> |
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.0/moment.min.js"></script> |
||||
|
||||
<style> |
||||
.vertical-center { |
||||
min-height: 100%; |
||||
min-height: 100vh; |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.progress { |
||||
position: relative; |
||||
} |
||||
.progress span { |
||||
position: absolute; |
||||
display: block; |
||||
width: 100%; |
||||
color: white; |
||||
} |
||||
pre { |
||||
padding: 6px; |
||||
margin: 0; |
||||
} |
||||
</style> |
||||
</head> |
||||
|
||||
<body> |
||||
<div class="vertical-center"> |
||||
<div class="container"> |
||||
<div class="row" style="margin-bottom: 16px;"> |
||||
<div class="col-lg-12"> |
||||
<h1 style="text-align: center;"><i class="fa fa-bath" aria-hidden="true"></i> {{.Network}} Authenticated Faucet</h1> |
||||
</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div class="col-lg-8 col-lg-offset-2"> |
||||
<div class="input-group"> |
||||
<input id="url" name="url" type="text" class="form-control" placeholder="Social network URL containing your Ethereum address..."/> |
||||
<span class="input-group-btn"> |
||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Give me Ether <i class="fa fa-caret-down" aria-hidden="true"></i></button> |
||||
<ul class="dropdown-menu dropdown-menu-right">{{range $idx, $amount := .Amounts}} |
||||
<li><a style="text-align: center;" onclick="tier={{$idx}}; {{if $.Recaptcha}}grecaptcha.execute(){{else}}submit({{$idx}}){{end}}">{{$amount}} / {{index $.Periods $idx}}</a></li>{{end}} |
||||
</ul> |
||||
</span> |
||||
</div>{{if .Recaptcha}} |
||||
<div class="g-recaptcha" data-sitekey="{{.Recaptcha}}" data-callback="submit" data-size="invisible"></div>{{end}} |
||||
</div> |
||||
</div> |
||||
<div class="row" style="margin-top: 32px;"> |
||||
<div class="col-lg-6 col-lg-offset-3"> |
||||
<div class="panel panel-small panel-default"> |
||||
<div class="panel-body" style="padding: 0; overflow: auto; max-height: 300px;"> |
||||
<table id="requests" class="table table-condensed" style="margin: 0;"></table> |
||||
</div> |
||||
<div class="panel-footer"> |
||||
<table style="width: 100%"><tr> |
||||
<td style="text-align: center;"><i class="fa fa-rss" aria-hidden="true"></i> <span id="peers"></span> peers</td> |
||||
<td style="text-align: center;"><i class="fa fa-database" aria-hidden="true"></i> <span id="block"></span> blocks</td> |
||||
<td style="text-align: center;"><i class="fa fa-heartbeat" aria-hidden="true"></i> <span id="funds"></span> Ethers</td> |
||||
<td style="text-align: center;"><i class="fa fa-university" aria-hidden="true"></i> <span id="funded"></span> funded</td> |
||||
</tr></table> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="row" style="margin-top: 32px;"> |
||||
<div class="col-lg-12"> |
||||
<h3>How does this work?</h3> |
||||
<p>This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to common 3rd party social network accounts. Anyone having a Twitter or Facebook account may request funds within the permitted limits.</p> |
||||
<dl class="dl-horizontal"> |
||||
<dt style="width: auto; margin-left: 40px;"><i class="fa fa-twitter" aria-hidden="true" style="font-size: 36px;"></i></dt> |
||||
<dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Twitter, make a <a href="https://twitter.com/intent/tweet?text=Requesting%20faucet%20funds%20into%200x0000000000000000000000000000000000000000%20on%20the%20%23{{.Network}}%20%23Ethereum%20test%20network." target="_about:blank">tweet</a> with your Ethereum address pasted into the contents (surrounding text doesn't matter).<br/>Copy-paste the <a href="https://support.twitter.com/articles/80586" target="_about:blank">tweets URL</a> into the above input box and fire away!</dd> |
||||
|
||||
<dt style="width: auto; margin-left: 40px;"><i class="fa fa-facebook" aria-hidden="true" style="font-size: 36px;"></i></dt> |
||||
<dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Facebook, publish a new <strong>public</strong> post with your Ethereum address embedded into the content (surrounding text doesn't matter).<br/>Copy-paste the <a href="https://www.facebook.com/help/community/question/?id=282662498552845" target="_about:blank">posts URL</a> into the above input box and fire away!</dd> |
||||
|
||||
{{if .NoAuth}} |
||||
<dt class="text-danger" style="width: auto; margin-left: 40px;"><i class="fa fa-unlock-alt" aria-hidden="true" style="font-size: 36px;"></i></dt> |
||||
<dd class="text-danger" style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds <strong>without authentication</strong>, simply copy-paste your Ethereum address into the above input box (surrounding text doesn't matter) and fire away.<br/>This mode is susceptible to Byzantine attacks. Only use for debugging or private networks!</dd> |
||||
{{end}} |
||||
</dl> |
||||
<p>You can track the current pending requests below the input field to see how much you have to wait until your turn comes.</p> |
||||
{{if .Recaptcha}}<em>The faucet is running invisible reCaptcha protection against bots.</em>{{end}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<script> |
||||
// Global variables to hold the current status of the faucet |
||||
var attempt = 0; |
||||
var server; |
||||
var tier = 0; |
||||
var requests = []; |
||||
|
||||
// Define a function that creates closures to drop old requests |
||||
var dropper = function(hash) { |
||||
return function() { |
||||
for (var i=0; i<requests.length; i++) { |
||||
if (requests[i].tx.hash == hash) { |
||||
requests.splice(i, 1); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
// Define the function that submits a gist url to the server |
||||
var submit = function({{if .Recaptcha}}captcha{{end}}) { |
||||
server.send(JSON.stringify({url: $("#url")[0].value, tier: tier{{if .Recaptcha}}, captcha: captcha{{end}}}));{{if .Recaptcha}} |
||||
grecaptcha.reset();{{end}} |
||||
}; |
||||
// Define a method to reconnect upon server loss |
||||
var reconnect = function() { |
||||
server = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/api"); |
||||
|
||||
server.onmessage = function(event) { |
||||
var msg = JSON.parse(event.data); |
||||
if (msg === null) { |
||||
return; |
||||
} |
||||
|
||||
if (msg.funds !== undefined) { |
||||
$("#funds").text(msg.funds); |
||||
} |
||||
if (msg.funded !== undefined) { |
||||
$("#funded").text(msg.funded); |
||||
} |
||||
if (msg.peers !== undefined) { |
||||
$("#peers").text(msg.peers); |
||||
} |
||||
if (msg.number !== undefined) { |
||||
$("#block").text(parseInt(msg.number, 16)); |
||||
} |
||||
if (msg.error !== undefined) { |
||||
noty({layout: 'topCenter', text: msg.error, type: 'error', timeout: 5000, progressBar: true}); |
||||
} |
||||
if (msg.success !== undefined) { |
||||
noty({layout: 'topCenter', text: msg.success, type: 'success', timeout: 5000, progressBar: true}); |
||||
} |
||||
if (msg.requests !== undefined && msg.requests !== null) { |
||||
// Mark all previous requests missing as done |
||||
for (var i=0; i<requests.length; i++) { |
||||
if (msg.requests.length > 0 && msg.requests[0].tx.hash == requests[i].tx.hash) { |
||||
break; |
||||
} |
||||
if (requests[i].time != "") { |
||||
requests[i].time = ""; |
||||
setTimeout(dropper(requests[i].tx.hash), 3000); |
||||
} |
||||
} |
||||
// Append any new requests into our local collection |
||||
var common = -1; |
||||
if (requests.length > 0) { |
||||
for (var i=0; i<msg.requests.length; i++) { |
||||
if (requests[requests.length-1].tx.hash == msg.requests[i].tx.hash) { |
||||
common = i; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
for (var i=common+1; i<msg.requests.length; i++) { |
||||
requests.push(msg.requests[i]); |
||||
} |
||||
// Iterate over our entire local collection and re-render the funding table |
||||
var content = ""; |
||||
for (var i=requests.length-1; i >= 0; i--) { |
||||
var done = requests[i].time == ""; |
||||
var elapsed = moment().unix()-moment(requests[i].time).unix(); |
||||
|
||||
content += "<tr id='" + requests[i].tx.hash + "'>"; |
||||
content += " <td><div style=\"background: url('" + requests[i].avatar + "'); background-size: cover; width:32px; height: 32px; border-radius: 4px;\"></div></td>"; |
||||
content += " <td><pre>" + requests[i].account + "</pre></td>"; |
||||
content += " <td style=\"width: 100%; text-align: center; vertical-align: middle;\">"; |
||||
if (done) { |
||||
content += " funded"; |
||||
} else { |
||||
content += " <span id='time-" + i + "' class='timer'>" + moment.duration(-elapsed, 'seconds').humanize(true) + "</span>"; |
||||
} |
||||
content += " <div class='progress' style='height: 4px; margin: 0;'>"; |
||||
if (done) { |
||||
content += " <div class='progress-bar progress-bar-success' role='progressbar' aria-valuenow='30' style='width:100%;'></div>"; |
||||
} else if (elapsed > 30) { |
||||
content += " <div class='progress-bar progress-bar-danger progress-bar-striped active' role='progressbar' aria-valuenow='30' style='width:100%;'></div>"; |
||||
} else { |
||||
content += " <div class='progress-bar progress-bar-striped active' role='progressbar' aria-valuenow='" + elapsed + "' style='width:" + (elapsed * 100 / 30) + "%;'></div>"; |
||||
} |
||||
content += " </div>"; |
||||
content += " </td>"; |
||||
content += "</tr>"; |
||||
} |
||||
$("#requests").html("<tbody>" + content + "</tbody>"); |
||||
} |
||||
} |
||||
server.onclose = function() { setTimeout(reconnect, 3000); }; |
||||
} |
||||
// Start a UI updater to push the progress bars forward until they are done |
||||
setInterval(function() { |
||||
$('.progress-bar').each(function() { |
||||
var progress = Number($(this).attr('aria-valuenow')) + 1; |
||||
if (progress < 30) { |
||||
$(this).attr('aria-valuenow', progress); |
||||
$(this).css('width', (progress * 100 / 30) + '%'); |
||||
} else if (progress == 30) { |
||||
$(this).css('width', '100%'); |
||||
$(this).addClass("progress-bar-danger"); |
||||
} |
||||
}) |
||||
$('.timer').each(function() { |
||||
var index = Number($(this).attr('id').substring(5)); |
||||
$(this).html(moment.duration(moment(requests[index].time).unix()-moment().unix(), 'seconds').humanize(true)); |
||||
}) |
||||
}, 1000); |
||||
|
||||
// Establish a websocket connection to the API server |
||||
reconnect(); |
||||
</script>{{if .Recaptcha}} |
||||
<script src="https://www.google.com/recaptcha/api.js" async defer></script>{{end}} |
||||
</body> |
||||
</html> |
@ -1,45 +0,0 @@ |
||||
// Copyright 2021 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
func TestFacebook(t *testing.T) { |
||||
// TODO: Remove facebook auth or implement facebook api, which seems to require an API key
|
||||
t.Skipf("The facebook access is flaky, needs to be reimplemented or removed") |
||||
for _, tt := range []struct { |
||||
url string |
||||
want common.Address |
||||
}{ |
||||
{ |
||||
"https://www.facebook.com/fooz.gazonk/posts/2837228539847129", |
||||
common.HexToAddress("0xDeadDeaDDeaDbEefbEeFbEEfBeeFBeefBeeFbEEF"), |
||||
}, |
||||
} { |
||||
_, _, gotAddress, err := authFacebook(tt.url) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if gotAddress != tt.want { |
||||
t.Fatalf("address wrong, have %v want %v", gotAddress, tt.want) |
||||
} |
||||
} |
||||
} |
@ -1,205 +0,0 @@ |
||||
// Copyright 2020 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"os" |
||||
"path/filepath" |
||||
"runtime" |
||||
"strings" |
||||
"sync/atomic" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/p2p" |
||||
"github.com/ethereum/go-ethereum/rpc" |
||||
) |
||||
|
||||
type gethrpc struct { |
||||
name string |
||||
rpc *rpc.Client |
||||
geth *testgeth |
||||
nodeInfo *p2p.NodeInfo |
||||
} |
||||
|
||||
func (g *gethrpc) killAndWait() { |
||||
g.geth.Kill() |
||||
g.geth.WaitExit() |
||||
} |
||||
|
||||
func (g *gethrpc) callRPC(result interface{}, method string, args ...interface{}) { |
||||
if err := g.rpc.Call(&result, method, args...); err != nil { |
||||
g.geth.Fatalf("callRPC %v: %v", method, err) |
||||
} |
||||
} |
||||
|
||||
func (g *gethrpc) addPeer(peer *gethrpc) { |
||||
g.geth.Logf("%v.addPeer(%v)", g.name, peer.name) |
||||
enode := peer.getNodeInfo().Enode |
||||
peerCh := make(chan *p2p.PeerEvent) |
||||
sub, err := g.rpc.Subscribe(context.Background(), "admin", peerCh, "peerEvents") |
||||
if err != nil { |
||||
g.geth.Fatalf("subscribe %v: %v", g.name, err) |
||||
} |
||||
defer sub.Unsubscribe() |
||||
g.callRPC(nil, "admin_addPeer", enode) |
||||
dur := 14 * time.Second |
||||
timeout := time.After(dur) |
||||
select { |
||||
case ev := <-peerCh: |
||||
g.geth.Logf("%v received event: type=%v, peer=%v", g.name, ev.Type, ev.Peer) |
||||
case err := <-sub.Err(): |
||||
g.geth.Fatalf("%v sub error: %v", g.name, err) |
||||
case <-timeout: |
||||
g.geth.Error("timeout adding peer after", dur) |
||||
} |
||||
} |
||||
|
||||
// Use this function instead of `g.nodeInfo` directly
|
||||
func (g *gethrpc) getNodeInfo() *p2p.NodeInfo { |
||||
if g.nodeInfo != nil { |
||||
return g.nodeInfo |
||||
} |
||||
g.nodeInfo = &p2p.NodeInfo{} |
||||
g.callRPC(&g.nodeInfo, "admin_nodeInfo") |
||||
return g.nodeInfo |
||||
} |
||||
|
||||
// ipcEndpoint resolves an IPC endpoint based on a configured value, taking into
|
||||
// account the set data folders as well as the designated platform we're currently
|
||||
// running on.
|
||||
func ipcEndpoint(ipcPath, datadir string) string { |
||||
// On windows we can only use plain top-level pipes
|
||||
if runtime.GOOS == "windows" { |
||||
if strings.HasPrefix(ipcPath, `\\.\pipe\`) { |
||||
return ipcPath |
||||
} |
||||
return `\\.\pipe\` + ipcPath |
||||
} |
||||
// Resolve names into the data directory full paths otherwise
|
||||
if filepath.Base(ipcPath) == ipcPath { |
||||
if datadir == "" { |
||||
return filepath.Join(os.TempDir(), ipcPath) |
||||
} |
||||
return filepath.Join(datadir, ipcPath) |
||||
} |
||||
return ipcPath |
||||
} |
||||
|
||||
// nextIPC ensures that each ipc pipe gets a unique name.
|
||||
// On linux, it works well to use ipc pipes all over the filesystem (in datadirs),
|
||||
// but windows require pipes to sit in "\\.\pipe\". Therefore, to run several
|
||||
// nodes simultaneously, we need to distinguish between them, which we do by
|
||||
// the pipe filename instead of folder.
|
||||
var nextIPC atomic.Uint32 |
||||
|
||||
func startGethWithIpc(t *testing.T, name string, args ...string) *gethrpc { |
||||
ipcName := fmt.Sprintf("geth-%d.ipc", nextIPC.Add(1)) |
||||
args = append([]string{"--networkid=42", "--port=0", "--authrpc.port", "0", "--ipcpath", ipcName}, args...) |
||||
t.Logf("Starting %v with rpc: %v", name, args) |
||||
|
||||
g := &gethrpc{ |
||||
name: name, |
||||
geth: runGeth(t, args...), |
||||
} |
||||
ipcpath := ipcEndpoint(ipcName, g.geth.Datadir) |
||||
// We can't know exactly how long geth will take to start, so we try 10
|
||||
// times over a 5 second period.
|
||||
var err error |
||||
for i := 0; i < 10; i++ { |
||||
time.Sleep(500 * time.Millisecond) |
||||
if g.rpc, err = rpc.Dial(ipcpath); err == nil { |
||||
return g |
||||
} |
||||
} |
||||
t.Fatalf("%v rpc connect to %v: %v", name, ipcpath, err) |
||||
return nil |
||||
} |
||||
|
||||
func initGeth(t *testing.T) string { |
||||
args := []string{"--networkid=42", "init", "./testdata/clique.json"} |
||||
t.Logf("Initializing geth: %v ", args) |
||||
g := runGeth(t, args...) |
||||
datadir := g.Datadir |
||||
g.WaitExit() |
||||
return datadir |
||||
} |
||||
|
||||
func startLightServer(t *testing.T) *gethrpc { |
||||
datadir := initGeth(t) |
||||
t.Logf("Importing keys to geth") |
||||
runGeth(t, "account", "import", "--datadir", datadir, "--password", "./testdata/password.txt", "--lightkdf", "./testdata/key.prv").WaitExit() |
||||
account := "0x02f0d131f1f97aef08aec6e3291b957d9efe7105" |
||||
server := startGethWithIpc(t, "lightserver", "--allow-insecure-unlock", "--datadir", datadir, "--password", "./testdata/password.txt", "--unlock", account, "--miner.etherbase=0x02f0d131f1f97aef08aec6e3291b957d9efe7105", "--mine", "--light.serve=100", "--light.maxpeers=1", "--discv4=false", "--nat=extip:127.0.0.1", "--verbosity=4") |
||||
return server |
||||
} |
||||
|
||||
func startClient(t *testing.T, name string) *gethrpc { |
||||
datadir := initGeth(t) |
||||
return startGethWithIpc(t, name, "--datadir", datadir, "--discv4=false", "--syncmode=light", "--nat=extip:127.0.0.1", "--verbosity=4") |
||||
} |
||||
|
||||
func TestPriorityClient(t *testing.T) { |
||||
lightServer := startLightServer(t) |
||||
defer lightServer.killAndWait() |
||||
|
||||
// Start client and add lightServer as peer
|
||||
freeCli := startClient(t, "freeCli") |
||||
defer freeCli.killAndWait() |
||||
freeCli.addPeer(lightServer) |
||||
|
||||
var peers []*p2p.PeerInfo |
||||
freeCli.callRPC(&peers, "admin_peers") |
||||
if len(peers) != 1 { |
||||
t.Errorf("Expected: # of client peers == 1, actual: %v", len(peers)) |
||||
return |
||||
} |
||||
|
||||
// Set up priority client, get its nodeID, increase its balance on the lightServer
|
||||
prioCli := startClient(t, "prioCli") |
||||
defer prioCli.killAndWait() |
||||
// 3_000_000_000 once we move to Go 1.13
|
||||
tokens := uint64(3000000000) |
||||
lightServer.callRPC(nil, "les_addBalance", prioCli.getNodeInfo().ID, tokens) |
||||
prioCli.addPeer(lightServer) |
||||
|
||||
// Check if priority client is actually syncing and the regular client got kicked out
|
||||
prioCli.callRPC(&peers, "admin_peers") |
||||
if len(peers) != 1 { |
||||
t.Errorf("Expected: # of prio peers == 1, actual: %v", len(peers)) |
||||
} |
||||
|
||||
nodes := map[string]*gethrpc{ |
||||
lightServer.getNodeInfo().ID: lightServer, |
||||
freeCli.getNodeInfo().ID: freeCli, |
||||
prioCli.getNodeInfo().ID: prioCli, |
||||
} |
||||
time.Sleep(1 * time.Second) |
||||
lightServer.callRPC(&peers, "admin_peers") |
||||
peersWithNames := make(map[string]string) |
||||
for _, p := range peers { |
||||
peersWithNames[nodes[p.ID].name] = p.ID |
||||
} |
||||
if _, freeClientFound := peersWithNames[freeCli.name]; freeClientFound { |
||||
t.Error("client is still a peer of lightServer", peersWithNames) |
||||
} |
||||
if _, prioClientFound := peersWithNames[prioCli.name]; !prioClientFound { |
||||
t.Error("prio client is not among lightServer peers", peersWithNames) |
||||
} |
||||
} |
@ -1,49 +1,52 @@ |
||||
{"111,222,333,444,555,678,999":"111222333444555678999","lvl":"info","msg":"big.Int","t":"2023-11-09T08:33:19.464383209+01:00"} |
||||
{"-111,222,333,444,555,678,999":"-111222333444555678999","lvl":"info","msg":"-big.Int","t":"2023-11-09T08:33:19.46455928+01:00"} |
||||
{"11,122,233,344,455,567,899,900":"11122233344455567899900","lvl":"info","msg":"big.Int","t":"2023-11-09T08:33:19.464582073+01:00"} |
||||
{"-11,122,233,344,455,567,899,900":"-11122233344455567899900","lvl":"info","msg":"-big.Int","t":"2023-11-09T08:33:19.464594846+01:00"} |
||||
{"111,222,333,444,555,678,999":"0x607851afc94ca2517","lvl":"info","msg":"uint256","t":"2023-11-09T08:33:19.464607873+01:00"} |
||||
{"11,122,233,344,455,567,899,900":"0x25aeffe8aaa1ef67cfc","lvl":"info","msg":"uint256","t":"2023-11-09T08:33:19.464694639+01:00"} |
||||
{"1,000,000":1000000,"lvl":"info","msg":"int64","t":"2023-11-09T08:33:19.464708835+01:00"} |
||||
{"-1,000,000":-1000000,"lvl":"info","msg":"int64","t":"2023-11-09T08:33:19.464725054+01:00"} |
||||
{"9,223,372,036,854,775,807":9223372036854775807,"lvl":"info","msg":"int64","t":"2023-11-09T08:33:19.464735773+01:00"} |
||||
{"-9,223,372,036,854,775,808":-9223372036854775808,"lvl":"info","msg":"int64","t":"2023-11-09T08:33:19.464744532+01:00"} |
||||
{"1,000,000":1000000,"lvl":"info","msg":"uint64","t":"2023-11-09T08:33:19.464752807+01:00"} |
||||
{"18,446,744,073,709,551,615":18446744073709551615,"lvl":"info","msg":"uint64","t":"2023-11-09T08:33:19.464779296+01:00"} |
||||
{"key":"special \r\n\t chars","lvl":"info","msg":"Special chars in value","t":"2023-11-09T08:33:19.464794181+01:00"} |
||||
{"lvl":"info","msg":"Special chars in key","special \n\t chars":"value","t":"2023-11-09T08:33:19.464827197+01:00"} |
||||
{"lvl":"info","msg":"nospace","nospace":"nospace","t":"2023-11-09T08:33:19.464841118+01:00"} |
||||
{"lvl":"info","msg":"with space","t":"2023-11-09T08:33:19.464862818+01:00","with nospace":"with nospace"} |
||||
{"key":"\u001b[1G\u001b[K\u001b[1A","lvl":"info","msg":"Bash escapes in value","t":"2023-11-09T08:33:19.464876802+01:00"} |
||||
{"\u001b[1G\u001b[K\u001b[1A":"value","lvl":"info","msg":"Bash escapes in key","t":"2023-11-09T08:33:19.464885416+01:00"} |
||||
{"key":"value","lvl":"info","msg":"Bash escapes in message \u001b[1G\u001b[K\u001b[1A end","t":"2023-11-09T08:33:19.464906946+01:00"} |
||||
{"\u001b[35mColored\u001b[0m[":"\u001b[35mColored\u001b[0m[","lvl":"info","msg":"\u001b[35mColored\u001b[0m[","t":"2023-11-09T08:33:19.464921455+01:00"} |
||||
{"2562047h47m16.854s":"2562047h47m16.854s","lvl":"info","msg":"Custom Stringer value","t":"2023-11-09T08:33:19.464943893+01:00"} |
||||
{"key":"lazy value","lvl":"info","msg":"Lazy evaluation of value","t":"2023-11-09T08:33:19.465013552+01:00"} |
||||
{"lvl":"info","msg":"A message with wonky 💩 characters","t":"2023-11-09T08:33:19.465069437+01:00"} |
||||
{"lvl":"info","msg":"A multiline message \nINFO [10-18|14:11:31.106] with wonky characters 💩","t":"2023-11-09T08:33:19.465083053+01:00"} |
||||
{"lvl":"info","msg":"A multiline message \nLALA [ZZZZZZZZZZZZZZZZZZ] Actually part of message above","t":"2023-11-09T08:33:19.465104289+01:00"} |
||||
{"false":"false","lvl":"info","msg":"boolean","t":"2023-11-09T08:33:19.465117185+01:00","true":"true"} |
||||
{"foo":"beta","lvl":"info","msg":"repeated-key 1","t":"2023-11-09T08:33:19.465143425+01:00"} |
||||
{"lvl":"info","msg":"repeated-key 2","t":"2023-11-09T08:33:19.465156323+01:00","xx":"longer"} |
||||
{"lvl":"info","msg":"log at level info","t":"2023-11-09T08:33:19.465193158+01:00"} |
||||
{"lvl":"warn","msg":"log at level warn","t":"2023-11-09T08:33:19.465228964+01:00"} |
||||
{"lvl":"eror","msg":"log at level error","t":"2023-11-09T08:33:19.465240352+01:00"} |
||||
{"a":"aligned left","bar":"short","lvl":"info","msg":"test","t":"2023-11-09T08:33:19.465247226+01:00"} |
||||
{"a":1,"bar":"a long message","lvl":"info","msg":"test","t":"2023-11-09T08:33:19.465269028+01:00"} |
||||
{"a":"aligned right","bar":"short","lvl":"info","msg":"test","t":"2023-11-09T08:33:19.465313611+01:00"} |
||||
{"lvl":"info","msg":"The following logs should align so that the key-fields make 5 columns","t":"2023-11-09T08:33:19.465328188+01:00"} |
||||
{"gas":1123123,"hash":"0x0000000000000000000000000000000000000000000000000000000000001234","lvl":"info","msg":"Inserted known block","number":1012,"other":"first","t":"2023-11-09T08:33:19.465350507+01:00","txs":200} |
||||
{"gas":1123,"hash":"0x0000000000000000000000000000000000000000000000000000000000001235","lvl":"info","msg":"Inserted new block","number":1,"other":"second","t":"2023-11-09T08:33:19.465387952+01:00","txs":2} |
||||
{"gas":1,"hash":"0x0000000000000000000000000000000000000000000000000000000000012322","lvl":"info","msg":"Inserted known block","number":99,"other":"third","t":"2023-11-09T08:33:19.465406687+01:00","txs":10} |
||||
{"gas":99,"hash":"0x0000000000000000000000000000000000000000000000000000000000001234","lvl":"warn","msg":"Inserted known block","number":1012,"other":"fourth","t":"2023-11-09T08:33:19.465433025+01:00","txs":200} |
||||
{"\u003cnil\u003e":"\u003cnil\u003e","lvl":"info","msg":"(*big.Int)(nil)","t":"2023-11-09T08:33:19.465450283+01:00"} |
||||
{"\u003cnil\u003e":"nil","lvl":"info","msg":"(*uint256.Int)(nil)","t":"2023-11-09T08:33:19.465472953+01:00"} |
||||
{"lvl":"info","msg":"(fmt.Stringer)(nil)","res":"\u003cnil\u003e","t":"2023-11-09T08:33:19.465538633+01:00"} |
||||
{"lvl":"info","msg":"nil-concrete-stringer","res":"nil","t":"2023-11-09T08:33:19.465552355+01:00"} |
||||
{"lvl":"info","msg":"error(nil) ","res":"\u003cnil\u003e","t":"2023-11-09T08:33:19.465601029+01:00"} |
||||
{"lvl":"info","msg":"nil-concrete-error","res":"","t":"2023-11-09T08:33:19.46561622+01:00"} |
||||
{"lvl":"info","msg":"nil-custom-struct","res":"\u003cnil\u003e","t":"2023-11-09T08:33:19.465638888+01:00"} |
||||
{"lvl":"info","msg":"raw nil","res":"\u003cnil\u003e","t":"2023-11-09T08:33:19.465673664+01:00"} |
||||
{"lvl":"info","msg":"(*uint64)(nil)","res":"\u003cnil\u003e","t":"2023-11-09T08:33:19.465700264+01:00"} |
||||
{"level":"level","lvl":"lvl","msg":"msg","t":"t","time":"time"} |
||||
{"t":"2023-11-22T15:42:00.407963+08:00","lvl":"info","msg":"big.Int","111,222,333,444,555,678,999":"111222333444555678999"} |
||||
{"t":"2023-11-22T15:42:00.408084+08:00","lvl":"info","msg":"-big.Int","-111,222,333,444,555,678,999":"-111222333444555678999"} |
||||
{"t":"2023-11-22T15:42:00.408092+08:00","lvl":"info","msg":"big.Int","11,122,233,344,455,567,899,900":"11122233344455567899900"} |
||||
{"t":"2023-11-22T15:42:00.408097+08:00","lvl":"info","msg":"-big.Int","-11,122,233,344,455,567,899,900":"-11122233344455567899900"} |
||||
{"t":"2023-11-22T15:42:00.408127+08:00","lvl":"info","msg":"uint256","111,222,333,444,555,678,999":"111222333444555678999"} |
||||
{"t":"2023-11-22T15:42:00.408133+08:00","lvl":"info","msg":"uint256","11,122,233,344,455,567,899,900":"11122233344455567899900"} |
||||
{"t":"2023-11-22T15:42:00.408137+08:00","lvl":"info","msg":"int64","1,000,000":1000000} |
||||
{"t":"2023-11-22T15:42:00.408145+08:00","lvl":"info","msg":"int64","-1,000,000":-1000000} |
||||
{"t":"2023-11-22T15:42:00.408149+08:00","lvl":"info","msg":"int64","9,223,372,036,854,775,807":9223372036854775807} |
||||
{"t":"2023-11-22T15:42:00.408153+08:00","lvl":"info","msg":"int64","-9,223,372,036,854,775,808":-9223372036854775808} |
||||
{"t":"2023-11-22T15:42:00.408156+08:00","lvl":"info","msg":"uint64","1,000,000":1000000} |
||||
{"t":"2023-11-22T15:42:00.40816+08:00","lvl":"info","msg":"uint64","18,446,744,073,709,551,615":18446744073709551615} |
||||
{"t":"2023-11-22T15:42:00.408164+08:00","lvl":"info","msg":"Special chars in value","key":"special \r\n\t chars"} |
||||
{"t":"2023-11-22T15:42:00.408167+08:00","lvl":"info","msg":"Special chars in key","special \n\t chars":"value"} |
||||
{"t":"2023-11-22T15:42:00.408171+08:00","lvl":"info","msg":"nospace","nospace":"nospace"} |
||||
{"t":"2023-11-22T15:42:00.408174+08:00","lvl":"info","msg":"with space","with nospace":"with nospace"} |
||||
{"t":"2023-11-22T15:42:00.408178+08:00","lvl":"info","msg":"Bash escapes in value","key":"\u001b[1G\u001b[K\u001b[1A"} |
||||
{"t":"2023-11-22T15:42:00.408182+08:00","lvl":"info","msg":"Bash escapes in key","\u001b[1G\u001b[K\u001b[1A":"value"} |
||||
{"t":"2023-11-22T15:42:00.408186+08:00","lvl":"info","msg":"Bash escapes in message \u001b[1G\u001b[K\u001b[1A end","key":"value"} |
||||
{"t":"2023-11-22T15:42:00.408194+08:00","lvl":"info","msg":"\u001b[35mColored\u001b[0m[","\u001b[35mColored\u001b[0m[":"\u001b[35mColored\u001b[0m["} |
||||
{"t":"2023-11-22T15:42:00.408197+08:00","lvl":"info","msg":"an error message with quotes","error":"this is an 'error'"} |
||||
{"t":"2023-11-22T15:42:00.408202+08:00","lvl":"info","msg":"Custom Stringer value","2562047h47m16.854s":"2562047h47m16.854s"} |
||||
{"t":"2023-11-22T15:42:00.408208+08:00","lvl":"info","msg":"a custom stringer that emits quoted text","output":"output with 'quotes'"} |
||||
{"t":"2023-11-22T15:42:00.408219+08:00","lvl":"info","msg":"A message with wonky 💩 characters"} |
||||
{"t":"2023-11-22T15:42:00.408222+08:00","lvl":"info","msg":"A multiline message \nINFO [10-18|14:11:31.106] with wonky characters 💩"} |
||||
{"t":"2023-11-22T15:42:00.408226+08:00","lvl":"info","msg":"A multiline message \nLALA [ZZZZZZZZZZZZZZZZZZ] Actually part of message above"} |
||||
{"t":"2023-11-22T15:42:00.408229+08:00","lvl":"info","msg":"boolean","true":true,"false":false} |
||||
{"t":"2023-11-22T15:42:00.408234+08:00","lvl":"info","msg":"repeated-key 1","foo":"alpha","foo":"beta"} |
||||
{"t":"2023-11-22T15:42:00.408237+08:00","lvl":"info","msg":"repeated-key 2","xx":"short","xx":"longer"} |
||||
{"t":"2023-11-22T15:42:00.408241+08:00","lvl":"info","msg":"log at level info"} |
||||
{"t":"2023-11-22T15:42:00.408244+08:00","lvl":"warn","msg":"log at level warn"} |
||||
{"t":"2023-11-22T15:42:00.408247+08:00","lvl":"eror","msg":"log at level error"} |
||||
{"t":"2023-11-22T15:42:00.408251+08:00","lvl":"info","msg":"test","bar":"short","a":"aligned left"} |
||||
{"t":"2023-11-22T15:42:00.408254+08:00","lvl":"info","msg":"test","bar":"a long message","a":1} |
||||
{"t":"2023-11-22T15:42:00.408258+08:00","lvl":"info","msg":"test","bar":"short","a":"aligned right"} |
||||
{"t":"2023-11-22T15:42:00.408261+08:00","lvl":"info","msg":"The following logs should align so that the key-fields make 5 columns"} |
||||
{"t":"2023-11-22T15:42:00.408275+08:00","lvl":"info","msg":"Inserted known block","number":1012,"hash":"0x0000000000000000000000000000000000000000000000000000000000001234","txs":200,"gas":1123123,"other":"first"} |
||||
{"t":"2023-11-22T15:42:00.408281+08:00","lvl":"info","msg":"Inserted new block","number":1,"hash":"0x0000000000000000000000000000000000000000000000000000000000001235","txs":2,"gas":1123,"other":"second"} |
||||
{"t":"2023-11-22T15:42:00.408287+08:00","lvl":"info","msg":"Inserted known block","number":99,"hash":"0x0000000000000000000000000000000000000000000000000000000000012322","txs":10,"gas":1,"other":"third"} |
||||
{"t":"2023-11-22T15:42:00.408296+08:00","lvl":"warn","msg":"Inserted known block","number":1012,"hash":"0x0000000000000000000000000000000000000000000000000000000000001234","txs":200,"gas":99,"other":"fourth"} |
||||
{"t":"2023-11-22T15:42:00.4083+08:00","lvl":"info","msg":"(*big.Int)(nil)","<nil>":"<nil>"} |
||||
{"t":"2023-11-22T15:42:00.408303+08:00","lvl":"info","msg":"(*uint256.Int)(nil)","<nil>":"<nil>"} |
||||
{"t":"2023-11-22T15:42:00.408311+08:00","lvl":"info","msg":"(fmt.Stringer)(nil)","res":null} |
||||
{"t":"2023-11-22T15:42:00.408318+08:00","lvl":"info","msg":"nil-concrete-stringer","res":"<nil>"} |
||||
{"t":"2023-11-22T15:42:00.408322+08:00","lvl":"info","msg":"error(nil) ","res":null} |
||||
{"t":"2023-11-22T15:42:00.408326+08:00","lvl":"info","msg":"nil-concrete-error","res":""} |
||||
{"t":"2023-11-22T15:42:00.408334+08:00","lvl":"info","msg":"nil-custom-struct","res":null} |
||||
{"t":"2023-11-22T15:42:00.40835+08:00","lvl":"info","msg":"raw nil","res":null} |
||||
{"t":"2023-11-22T15:42:00.408354+08:00","lvl":"info","msg":"(*uint64)(nil)","res":null} |
||||
{"t":"2023-11-22T15:42:00.408361+08:00","lvl":"info","msg":"Using keys 't', 'lvl', 'time', 'level' and 'msg'","t":"t","time":"time","lvl":"lvl","level":"level","msg":"msg"} |
||||
{"t":"2023-11-29T15:13:00.195655931+01:00","lvl":"info","msg":"Odd pair (1 attr)","key":null,"LOG_ERROR":"Normalized odd number of arguments by adding nil"} |
||||
{"t":"2023-11-29T15:13:00.195681832+01:00","lvl":"info","msg":"Odd pair (3 attr)","key":"value","key2":null,"LOG_ERROR":"Normalized odd number of arguments by adding nil"} |
||||
|
@ -1,49 +1,52 @@ |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=big.Int 111,222,333,444,555,678,999=111,222,333,444,555,678,999 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=-big.Int -111,222,333,444,555,678,999=-111,222,333,444,555,678,999 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=big.Int 11,122,233,344,455,567,899,900=11,122,233,344,455,567,899,900 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=-big.Int -11,122,233,344,455,567,899,900=-11,122,233,344,455,567,899,900 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=uint256 111,222,333,444,555,678,999=111,222,333,444,555,678,999 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=uint256 11,122,233,344,455,567,899,900=11,122,233,344,455,567,899,900 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=int64 1,000,000=1,000,000 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=int64 -1,000,000=-1,000,000 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=int64 9,223,372,036,854,775,807=9,223,372,036,854,775,807 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=int64 -9,223,372,036,854,775,808=-9,223,372,036,854,775,808 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=uint64 1,000,000=1,000,000 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=uint64 18,446,744,073,709,551,615=18,446,744,073,709,551,615 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="Special chars in value" key="special \r\n\t chars" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="Special chars in key" "special \n\t chars"=value |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=nospace nospace=nospace |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="with space" "with nospace"="with nospace" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="Bash escapes in value" key="\x1b[1G\x1b[K\x1b[1A" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="Bash escapes in key" "\x1b[1G\x1b[K\x1b[1A"=value |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="Bash escapes in message \x1b[1G\x1b[K\x1b[1A end" key=value |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="\x1b[35mColored\x1b[0m[" "\x1b[35mColored\x1b[0m["="\x1b[35mColored\x1b[0m[" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="Custom Stringer value" 2562047h47m16.854s=2562047h47m16.854s |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="Lazy evaluation of value" key="lazy value" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="A message with wonky 💩 characters" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="A multiline message \nINFO [10-18|14:11:31.106] with wonky characters 💩" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="A multiline message \nLALA [ZZZZZZZZZZZZZZZZZZ] Actually part of message above" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=boolean true=true false=false |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="repeated-key 1" foo=alpha foo=beta |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="repeated-key 2" xx=short xx=longer |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="log at level info" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=warn msg="log at level warn" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=eror msg="log at level error" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=test bar=short a="aligned left" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=test bar="a long message" a=1 |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=test bar=short a="aligned right" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="The following logs should align so that the key-fields make 5 columns" |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="Inserted known block" number=1012 hash=0x0000000000000000000000000000000000000000000000000000000000001234 txs=200 gas=1,123,123 other=first |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="Inserted new block" number=1 hash=0x0000000000000000000000000000000000000000000000000000000000001235 txs=2 gas=1123 other=second |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="Inserted known block" number=99 hash=0x0000000000000000000000000000000000000000000000000000000000012322 txs=10 gas=1 other=third |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=warn msg="Inserted known block" number=1012 hash=0x0000000000000000000000000000000000000000000000000000000000001234 txs=200 gas=99 other=fourth |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=(*big.Int)(nil) <nil>=<nil> |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=(*uint256.Int)(nil) <nil>=<nil> |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=(fmt.Stringer)(nil) res=nil |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=nil-concrete-stringer res=nil |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="error(nil) " res=nil |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=nil-concrete-error res= |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=nil-custom-struct res=<nil> |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="raw nil" res=nil |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg=(*uint64)(nil) res=<nil> |
||||
t=xxxxxxxxxxxxxxxxxxxxxxxx lvl=info msg="Using keys 't', 'lvl', 'time', 'level' and 'msg'" t=t time=time lvl=lvl level=level msg=msg |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=big.Int 111,222,333,444,555,678,999=111222333444555678999 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=-big.Int -111,222,333,444,555,678,999=-111222333444555678999 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=big.Int 11,122,233,344,455,567,899,900=11122233344455567899900 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=-big.Int -11,122,233,344,455,567,899,900=-11122233344455567899900 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=uint256 111,222,333,444,555,678,999=111222333444555678999 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=uint256 11,122,233,344,455,567,899,900=11122233344455567899900 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=int64 1,000,000=1000000 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=int64 -1,000,000=-1000000 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=int64 9,223,372,036,854,775,807=9223372036854775807 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=int64 -9,223,372,036,854,775,808=-9223372036854775808 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=uint64 1,000,000=1000000 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=uint64 18,446,744,073,709,551,615=18446744073709551615 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="Special chars in value" key="special \r\n\t chars" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="Special chars in key" "special \n\t chars"=value |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=nospace nospace=nospace |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="with space" "with nospace"="with nospace" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="Bash escapes in value" key="\x1b[1G\x1b[K\x1b[1A" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="Bash escapes in key" "\x1b[1G\x1b[K\x1b[1A"=value |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="Bash escapes in message \x1b[1G\x1b[K\x1b[1A end" key=value |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="\x1b[35mColored\x1b[0m[" "\x1b[35mColored\x1b[0m["="\x1b[35mColored\x1b[0m[" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="an error message with quotes" error="this is an 'error'" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="Custom Stringer value" 2562047h47m16.854s=2562047h47m16.854s |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="a custom stringer that emits quoted text" output="output with 'quotes'" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="A message with wonky 💩 characters" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="A multiline message \nINFO [10-18|14:11:31.106] with wonky characters 💩" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="A multiline message \nLALA [ZZZZZZZZZZZZZZZZZZ] Actually part of message above" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=boolean true=true false=false |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="repeated-key 1" foo=alpha foo=beta |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="repeated-key 2" xx=short xx=longer |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="log at level info" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=warn msg="log at level warn" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=eror msg="log at level error" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=test bar=short a="aligned left" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=test bar="a long message" a=1 |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=test bar=short a="aligned right" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="The following logs should align so that the key-fields make 5 columns" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="Inserted known block" number=1012 hash=0x0000000000000000000000000000000000000000000000000000000000001234 txs=200 gas=1123123 other=first |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="Inserted new block" number=1 hash=0x0000000000000000000000000000000000000000000000000000000000001235 txs=2 gas=1123 other=second |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="Inserted known block" number=99 hash=0x0000000000000000000000000000000000000000000000000000000000012322 txs=10 gas=1 other=third |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=warn msg="Inserted known block" number=1012 hash=0x0000000000000000000000000000000000000000000000000000000000001234 txs=200 gas=99 other=fourth |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=(*big.Int)(nil) <nil>=<nil> |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=(*uint256.Int)(nil) <nil>=<nil> |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=(fmt.Stringer)(nil) res=<nil> |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=nil-concrete-stringer res=<nil> |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="error(nil) " res=<nil> |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=nil-concrete-error res="" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=nil-custom-struct res=<nil> |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="raw nil" res=<nil> |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg=(*uint64)(nil) res=<nil> |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="Using keys 't', 'lvl', 'time', 'level' and 'msg'" t=t time=time lvl=lvl level=level msg=msg |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="Odd pair (1 attr)" key=<nil> LOG_ERROR="Normalized odd number of arguments by adding nil" |
||||
t=xxxx-xx-xxTxx:xx:xx+xxxx lvl=info msg="Odd pair (3 attr)" key=value key2=<nil> LOG_ERROR="Normalized odd number of arguments by adding nil" |
||||
|
@ -1,50 +1,53 @@ |
||||
INFO [XX-XX|XX:XX:XX.XXX] big.Int 111,222,333,444,555,678,999=111,222,333,444,555,678,999 |
||||
INFO [XX-XX|XX:XX:XX.XXX] -big.Int -111,222,333,444,555,678,999=-111,222,333,444,555,678,999 |
||||
INFO [XX-XX|XX:XX:XX.XXX] big.Int 11,122,233,344,455,567,899,900=11,122,233,344,455,567,899,900 |
||||
INFO [XX-XX|XX:XX:XX.XXX] -big.Int -11,122,233,344,455,567,899,900=-11,122,233,344,455,567,899,900 |
||||
INFO [XX-XX|XX:XX:XX.XXX] uint256 111,222,333,444,555,678,999=111,222,333,444,555,678,999 |
||||
INFO [XX-XX|XX:XX:XX.XXX] uint256 11,122,233,344,455,567,899,900=11,122,233,344,455,567,899,900 |
||||
INFO [XX-XX|XX:XX:XX.XXX] int64 1,000,000=1,000,000 |
||||
INFO [XX-XX|XX:XX:XX.XXX] int64 -1,000,000=-1,000,000 |
||||
INFO [XX-XX|XX:XX:XX.XXX] int64 9,223,372,036,854,775,807=9,223,372,036,854,775,807 |
||||
INFO [XX-XX|XX:XX:XX.XXX] int64 -9,223,372,036,854,775,808=-9,223,372,036,854,775,808 |
||||
INFO [XX-XX|XX:XX:XX.XXX] uint64 1,000,000=1,000,000 |
||||
INFO [XX-XX|XX:XX:XX.XXX] uint64 18,446,744,073,709,551,615=18,446,744,073,709,551,615 |
||||
INFO [XX-XX|XX:XX:XX.XXX] Special chars in value key="special \r\n\t chars" |
||||
INFO [XX-XX|XX:XX:XX.XXX] Special chars in key "special \n\t chars"=value |
||||
INFO [XX-XX|XX:XX:XX.XXX] nospace nospace=nospace |
||||
INFO [XX-XX|XX:XX:XX.XXX] with space "with nospace"="with nospace" |
||||
INFO [XX-XX|XX:XX:XX.XXX] Bash escapes in value key="\x1b[1G\x1b[K\x1b[1A" |
||||
INFO [XX-XX|XX:XX:XX.XXX] Bash escapes in key "\x1b[1G\x1b[K\x1b[1A"=value |
||||
INFO [XX-XX|XX:XX:XX.XXX] "Bash escapes in message \x1b[1G\x1b[K\x1b[1A end" key=value |
||||
INFO [XX-XX|XX:XX:XX.XXX] "\x1b[35mColored\x1b[0m[" "\x1b[35mColored\x1b[0m["="\x1b[35mColored\x1b[0m[" |
||||
INFO [XX-XX|XX:XX:XX.XXX] Custom Stringer value 2562047h47m16.854s=2562047h47m16.854s |
||||
INFO [XX-XX|XX:XX:XX.XXX] Lazy evaluation of value key="lazy value" |
||||
INFO [XX-XX|XX:XX:XX.XXX] "A message with wonky 💩 characters" |
||||
INFO [XX-XX|XX:XX:XX.XXX] "A multiline message \nINFO [10-18|14:11:31.106] with wonky characters 💩" |
||||
INFO [XX-XX|XX:XX:XX.XXX] A multiline message |
||||
LALA [XXZXXZXXZXXZXXZXXX] Actually part of message above |
||||
INFO [XX-XX|XX:XX:XX.XXX] boolean true=true false=false |
||||
INFO [XX-XX|XX:XX:XX.XXX] repeated-key 1 foo=alpha foo=beta |
||||
INFO [XX-XX|XX:XX:XX.XXX] repeated-key 2 xx=short xx=longer |
||||
INFO [XX-XX|XX:XX:XX.XXX] log at level info |
||||
WARN [XX-XX|XX:XX:XX.XXX] log at level warn |
||||
ERROR[XX-XX|XX:XX:XX.XXX] log at level error |
||||
INFO [XX-XX|XX:XX:XX.XXX] test bar=short a="aligned left" |
||||
INFO [XX-XX|XX:XX:XX.XXX] test bar="a long message" a=1 |
||||
INFO [XX-XX|XX:XX:XX.XXX] test bar=short a="aligned right" |
||||
INFO [XX-XX|XX:XX:XX.XXX] The following logs should align so that the key-fields make 5 columns |
||||
INFO [XX-XX|XX:XX:XX.XXX] Inserted known block number=1012 hash=000000..001234 txs=200 gas=1,123,123 other=first |
||||
INFO [XX-XX|XX:XX:XX.XXX] Inserted new block number=1 hash=000000..001235 txs=2 gas=1123 other=second |
||||
INFO [XX-XX|XX:XX:XX.XXX] Inserted known block number=99 hash=000000..012322 txs=10 gas=1 other=third |
||||
WARN [XX-XX|XX:XX:XX.XXX] Inserted known block number=1012 hash=000000..001234 txs=200 gas=99 other=fourth |
||||
INFO [XX-XX|XX:XX:XX.XXX] (*big.Int)(nil) <nil>=<nil> |
||||
INFO [XX-XX|XX:XX:XX.XXX] (*uint256.Int)(nil) <nil>=<nil> |
||||
INFO [XX-XX|XX:XX:XX.XXX] (fmt.Stringer)(nil) res=nil |
||||
INFO [XX-XX|XX:XX:XX.XXX] nil-concrete-stringer res=nil |
||||
INFO [XX-XX|XX:XX:XX.XXX] error(nil) res=nil |
||||
INFO [XX-XX|XX:XX:XX.XXX] nil-concrete-error res= |
||||
INFO [XX-XX|XX:XX:XX.XXX] nil-custom-struct res=<nil> |
||||
INFO [XX-XX|XX:XX:XX.XXX] raw nil res=nil |
||||
INFO [XX-XX|XX:XX:XX.XXX] (*uint64)(nil) res=<nil> |
||||
INFO [XX-XX|XX:XX:XX.XXX] Using keys 't', 'lvl', 'time', 'level' and 'msg' t=t time=time lvl=lvl level=level msg=msg |
||||
INFO [xx-xx|xx:xx:xx.xxx] big.Int 111,222,333,444,555,678,999=111,222,333,444,555,678,999 |
||||
INFO [xx-xx|xx:xx:xx.xxx] -big.Int -111,222,333,444,555,678,999=-111,222,333,444,555,678,999 |
||||
INFO [xx-xx|xx:xx:xx.xxx] big.Int 11,122,233,344,455,567,899,900=11,122,233,344,455,567,899,900 |
||||
INFO [xx-xx|xx:xx:xx.xxx] -big.Int -11,122,233,344,455,567,899,900=-11,122,233,344,455,567,899,900 |
||||
INFO [xx-xx|xx:xx:xx.xxx] uint256 111,222,333,444,555,678,999=111,222,333,444,555,678,999 |
||||
INFO [xx-xx|xx:xx:xx.xxx] uint256 11,122,233,344,455,567,899,900=11,122,233,344,455,567,899,900 |
||||
INFO [xx-xx|xx:xx:xx.xxx] int64 1,000,000=1,000,000 |
||||
INFO [xx-xx|xx:xx:xx.xxx] int64 -1,000,000=-1,000,000 |
||||
INFO [xx-xx|xx:xx:xx.xxx] int64 9,223,372,036,854,775,807=9,223,372,036,854,775,807 |
||||
INFO [xx-xx|xx:xx:xx.xxx] int64 -9,223,372,036,854,775,808=-9,223,372,036,854,775,808 |
||||
INFO [xx-xx|xx:xx:xx.xxx] uint64 1,000,000=1,000,000 |
||||
INFO [xx-xx|xx:xx:xx.xxx] uint64 18,446,744,073,709,551,615=18,446,744,073,709,551,615 |
||||
INFO [xx-xx|xx:xx:xx.xxx] Special chars in value key="special \r\n\t chars" |
||||
INFO [xx-xx|xx:xx:xx.xxx] Special chars in key "special \n\t chars"=value |
||||
INFO [xx-xx|xx:xx:xx.xxx] nospace nospace=nospace |
||||
INFO [xx-xx|xx:xx:xx.xxx] with space "with nospace"="with nospace" |
||||
INFO [xx-xx|xx:xx:xx.xxx] Bash escapes in value key="\x1b[1G\x1b[K\x1b[1A" |
||||
INFO [xx-xx|xx:xx:xx.xxx] Bash escapes in key "\x1b[1G\x1b[K\x1b[1A"=value |
||||
INFO [xx-xx|xx:xx:xx.xxx] "Bash escapes in message \x1b[1G\x1b[K\x1b[1A end" key=value |
||||
INFO [xx-xx|xx:xx:xx.xxx] "\x1b[35mColored\x1b[0m[" "\x1b[35mColored\x1b[0m["="\x1b[35mColored\x1b[0m[" |
||||
INFO [xx-xx|xx:xx:xx.xxx] an error message with quotes error="this is an 'error'" |
||||
INFO [xx-xx|xx:xx:xx.xxx] Custom Stringer value 2562047h47m16.854s=2562047h47m16.854s |
||||
INFO [xx-xx|xx:xx:xx.xxx] a custom stringer that emits quoted text output="output with 'quotes'" |
||||
INFO [xx-xx|xx:xx:xx.xxx] "A message with wonky 💩 characters" |
||||
INFO [xx-xx|xx:xx:xx.xxx] "A multiline message \nINFO [10-18|14:11:31.106] with wonky characters 💩" |
||||
INFO [xx-xx|xx:xx:xx.xxx] A multiline message |
||||
LALA [ZZZZZZZZZZZZZZZZZZ] Actually part of message above |
||||
INFO [xx-xx|xx:xx:xx.xxx] boolean true=true false=false |
||||
INFO [xx-xx|xx:xx:xx.xxx] repeated-key 1 foo=alpha foo=beta |
||||
INFO [xx-xx|xx:xx:xx.xxx] repeated-key 2 xx=short xx=longer |
||||
INFO [xx-xx|xx:xx:xx.xxx] log at level info |
||||
WARN [xx-xx|xx:xx:xx.xxx] log at level warn |
||||
ERROR[xx-xx|xx:xx:xx.xxx] log at level error |
||||
INFO [xx-xx|xx:xx:xx.xxx] test bar=short a="aligned left" |
||||
INFO [xx-xx|xx:xx:xx.xxx] test bar="a long message" a=1 |
||||
INFO [xx-xx|xx:xx:xx.xxx] test bar=short a="aligned right" |
||||
INFO [xx-xx|xx:xx:xx.xxx] The following logs should align so that the key-fields make 5 columns |
||||
INFO [xx-xx|xx:xx:xx.xxx] Inserted known block number=1012 hash=000000..001234 txs=200 gas=1,123,123 other=first |
||||
INFO [xx-xx|xx:xx:xx.xxx] Inserted new block number=1 hash=000000..001235 txs=2 gas=1123 other=second |
||||
INFO [xx-xx|xx:xx:xx.xxx] Inserted known block number=99 hash=000000..012322 txs=10 gas=1 other=third |
||||
WARN [xx-xx|xx:xx:xx.xxx] Inserted known block number=1012 hash=000000..001234 txs=200 gas=99 other=fourth |
||||
INFO [xx-xx|xx:xx:xx.xxx] (*big.Int)(nil) <nil>=<nil> |
||||
INFO [xx-xx|xx:xx:xx.xxx] (*uint256.Int)(nil) <nil>=<nil> |
||||
INFO [xx-xx|xx:xx:xx.xxx] (fmt.Stringer)(nil) res=<nil> |
||||
INFO [xx-xx|xx:xx:xx.xxx] nil-concrete-stringer res=<nil> |
||||
INFO [xx-xx|xx:xx:xx.xxx] error(nil) res=<nil> |
||||
INFO [xx-xx|xx:xx:xx.xxx] nil-concrete-error res= |
||||
INFO [xx-xx|xx:xx:xx.xxx] nil-custom-struct res=<nil> |
||||
INFO [xx-xx|xx:xx:xx.xxx] raw nil res=<nil> |
||||
INFO [xx-xx|xx:xx:xx.xxx] (*uint64)(nil) res=<nil> |
||||
INFO [xx-xx|xx:xx:xx.xxx] Using keys 't', 'lvl', 'time', 'level' and 'msg' t=t time=time lvl=lvl level=level msg=msg |
||||
INFO [xx-xx|xx:xx:xx.xxx] Odd pair (1 attr) key=<nil> LOG_ERROR="Normalized odd number of arguments by adding nil" |
||||
INFO [xx-xx|xx:xx:xx.xxx] Odd pair (3 attr) key=value key2=<nil> LOG_ERROR="Normalized odd number of arguments by adding nil" |
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue