Skip to content

Commit e1e6598

Browse files
authored
Merge pull request #6057 from oasisprotocol/ptrus/stable/20.12.x/archive
[BACKPORT/20.12.x] Add consensus archive mode support
2 parents b3e5b50 + 9898010 commit e1e6598

16 files changed

Lines changed: 1262 additions & 704 deletions

File tree

.changelog/4539.feature.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Add archive mode support
2+
3+
Node started in archive mode only serves existing consensus and runtime
4+
states. The node has all unneeded consensus and P2P functionality disabled so
5+
it wont participate in the network. Archive mode can be set using the
6+
`consensus.tendermint.mode` setting.

go/consensus/api/api.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package api
44

55
import (
66
"context"
7+
"fmt"
78
"strings"
89
"time"
910

@@ -34,6 +35,61 @@ const (
3435
HeightLatest int64 = 0
3536
)
3637

38+
// Mode is the consensus node mode.
39+
type Mode string
40+
41+
const (
42+
// ModeFull is the name of the full node consensus mode.
43+
ModeFull Mode = "full"
44+
// ModeSeed is the name of the seed-only node consensus mode.
45+
ModeSeed Mode = "seed"
46+
// ModeArchive is the name of the archive node consensus mode.
47+
ModeArchive Mode = "archive"
48+
)
49+
50+
// MarshalText encodes a Mode into text form.
51+
func (m Mode) MarshalText() ([]byte, error) {
52+
switch m {
53+
case ModeFull:
54+
return []byte(ModeFull.String()), nil
55+
case ModeSeed:
56+
return []byte(ModeSeed.String()), nil
57+
case ModeArchive:
58+
return []byte(ModeArchive.String()), nil
59+
default:
60+
return nil, fmt.Errorf("invalid mode: %s", string(m))
61+
}
62+
}
63+
64+
// UnmarshalText decodes a text marshaled consensus mode.
65+
func (m *Mode) UnmarshalText(text []byte) error {
66+
switch string(text) {
67+
case ModeFull.String():
68+
*m = ModeFull
69+
case ModeSeed.String():
70+
*m = ModeSeed
71+
case ModeArchive.String():
72+
*m = ModeArchive
73+
default:
74+
return fmt.Errorf("invalid consensus mode: %s", string(text))
75+
}
76+
return nil
77+
}
78+
79+
// String returns a string representation of the mode.
80+
func (m Mode) String() string {
81+
switch m {
82+
case ModeFull:
83+
return string(ModeFull)
84+
case ModeSeed:
85+
return string(ModeSeed)
86+
case ModeArchive:
87+
return string(ModeArchive)
88+
default:
89+
return fmt.Sprintf("[unknown consensus mode: %s]", string(m))
90+
}
91+
}
92+
3793
var (
3894
// ErrNoCommittedBlocks is the error returned when there are no committed
3995
// blocks and as such no state can be queried.

go/consensus/api/api_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package api
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestConsensusMode(t *testing.T) {
10+
require := require.New(t)
11+
12+
// Test valid Modes.
13+
for _, k := range []Mode{
14+
ModeArchive,
15+
ModeFull,
16+
ModeSeed,
17+
} {
18+
enc, err := k.MarshalText()
19+
require.NoError(err, "MarshalText")
20+
21+
var s Mode
22+
err = s.UnmarshalText(enc)
23+
require.NoError(err, "UnmarshalText")
24+
25+
require.Equal(k, s, "consensus mode should round-trip")
26+
}
27+
28+
// Test invalid Mode.
29+
sr := Mode("abc")
30+
require.Equal("[unknown consensus mode: abc]", sr.String())
31+
enc, err := sr.MarshalText()
32+
require.Nil(enc, "MarshalText on invalid consensus mode should be nil")
33+
require.Error(err, "MarshalText on invalid consensus mode should error")
34+
35+
err = sr.UnmarshalText([]byte("invalid consensus mode"))
36+
require.Error(err, "UnmarshalText on invalid consensus mode should error")
37+
}

go/consensus/api/submission.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ func (pd *staticPriceDiscovery) GasPrice(ctx context.Context) (*quantity.Quantit
4444
return pd.price.Clone(), nil
4545
}
4646

47+
type noOpPriceDiscovery struct{}
48+
49+
func (pd *noOpPriceDiscovery) GasPrice(ctx context.Context) (*quantity.Quantity, error) {
50+
return nil, transaction.ErrMethodNotSupported
51+
}
52+
4753
// SubmissionManager is a transaction submission manager interface.
4854
type SubmissionManager interface {
4955
// SignAndSubmitTx populates the nonce and fee fields in the transaction, signs the transaction
@@ -170,3 +176,11 @@ func NewSubmissionManager(backend ClientBackend, priceDiscovery PriceDiscovery,
170176
func SignAndSubmitTx(ctx context.Context, backend Backend, signer signature.Signer, tx *transaction.Transaction) error {
171177
return backend.SubmissionManager().SignAndSubmitTx(ctx, signer, tx)
172178
}
179+
180+
// NoOpSubmissionManager implements a submission manager that doesn't support submitting transactions.
181+
type NoOpSubmissionManager struct{}
182+
183+
// Implements SubmissionManager.
184+
func (m *NoOpSubmissionManager) SignAndSubmitTx(ctx context.Context, signer signature.Signer, tx *transaction.Transaction) error {
185+
return transaction.ErrMethodNotSupported
186+
}

go/consensus/api/transaction/transaction.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ var (
2323
// ErrInvalidNonce is the error returned when a nonce is invalid.
2424
ErrInvalidNonce = errors.New(moduleName, 1, "transaction: invalid nonce")
2525

26+
// ErrMethodNotSupported is the error returned if transaction method is not supported.
27+
ErrMethodNotSupported = errors.New(moduleName, 5, "transaction: method not supported")
28+
2629
// SignatureContext is the context used for signing transactions.
2730
SignatureContext = signature.NewContext("oasis-core/consensus: tx", signature.WithChainSeparation())
2831

go/consensus/tendermint/api/api.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ type Backend interface {
199199

200200
// WatchTendermintBlocks returns a stream of Tendermint blocks as they are
201201
// returned via the `EventDataNewBlock` query.
202-
WatchTendermintBlocks() (<-chan *tmtypes.Block, *pubsub.Subscription)
202+
WatchTendermintBlocks() (<-chan *tmtypes.Block, *pubsub.Subscription, error)
203203

204204
// GetLastRetainedVersion returns the earliest retained version the ABCI
205205
// state.
@@ -308,8 +308,7 @@ type ServiceClient interface {
308308

309309
// BaseServiceClient is a default ServiceClient implementation that provides noop implementations of
310310
// all the delivery methods. Implementations should override them as needed.
311-
type BaseServiceClient struct {
312-
}
311+
type BaseServiceClient struct{}
313312

314313
// Implements ServiceClient.
315314
func (bsc *BaseServiceClient) DeliverBlock(ctx context.Context, height int64) error {
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package full
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"path/filepath"
7+
"sync"
8+
"time"
9+
10+
"github.com/spf13/viper"
11+
abcicli "github.com/tendermint/tendermint/abci/client"
12+
tmconfig "github.com/tendermint/tendermint/config"
13+
tmsync "github.com/tendermint/tendermint/libs/sync"
14+
tmnode "github.com/tendermint/tendermint/node"
15+
tmproxy "github.com/tendermint/tendermint/proxy"
16+
tmcore "github.com/tendermint/tendermint/rpc/core"
17+
tmrpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
18+
"github.com/tendermint/tendermint/store"
19+
20+
"github.com/oasisprotocol/oasis-core/go/common/identity"
21+
"github.com/oasisprotocol/oasis-core/go/common/logging"
22+
cmservice "github.com/oasisprotocol/oasis-core/go/common/service"
23+
consensusAPI "github.com/oasisprotocol/oasis-core/go/consensus/api"
24+
"github.com/oasisprotocol/oasis-core/go/consensus/api/transaction"
25+
"github.com/oasisprotocol/oasis-core/go/consensus/tendermint/abci"
26+
"github.com/oasisprotocol/oasis-core/go/consensus/tendermint/api"
27+
tmcommon "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/common"
28+
"github.com/oasisprotocol/oasis-core/go/consensus/tendermint/db"
29+
genesisAPI "github.com/oasisprotocol/oasis-core/go/genesis/api"
30+
cmbackground "github.com/oasisprotocol/oasis-core/go/oasis-node/cmd/common/background"
31+
)
32+
33+
var _ api.Backend = (*archiveService)(nil)
34+
35+
type archiveService struct {
36+
sync.Mutex
37+
commonNode
38+
39+
abciClient abcicli.Client
40+
41+
isStarted bool
42+
43+
startedCh chan struct{}
44+
quitCh chan struct{}
45+
46+
stopOnce sync.Once
47+
}
48+
49+
func (srv *archiveService) started() bool {
50+
srv.Lock()
51+
defer srv.Unlock()
52+
53+
return srv.isStarted
54+
}
55+
56+
// Start starts the service.
57+
func (srv *archiveService) Start() error {
58+
if srv.started() {
59+
return fmt.Errorf("tendermint: service already started")
60+
}
61+
62+
if err := srv.commonNode.Start(); err != nil {
63+
return err
64+
}
65+
66+
if err := srv.abciClient.Start(); err != nil {
67+
return err
68+
}
69+
70+
// Make sure the quit channel is closed when the node shuts down.
71+
go func() {
72+
select {
73+
case <-srv.quitCh:
74+
case <-srv.mux.Quit():
75+
select {
76+
case <-srv.quitCh:
77+
default:
78+
close(srv.quitCh)
79+
}
80+
}
81+
}()
82+
83+
srv.Lock()
84+
srv.isStarted = true
85+
srv.Unlock()
86+
close(srv.startedCh)
87+
88+
return nil
89+
}
90+
91+
// Stop halts the service.
92+
func (srv *archiveService) Stop() {
93+
if !srv.started() {
94+
return
95+
}
96+
srv.stopOnce.Do(func() {
97+
if err := srv.abciClient.Stop(); err != nil {
98+
srv.Logger.Error("error on stopping abci client", "err", err)
99+
}
100+
srv.commonNode.Stop()
101+
})
102+
}
103+
104+
// Quit returns a channel that will be closed when the service terminates.
105+
func (srv *archiveService) Quit() <-chan struct{} {
106+
return srv.quitCh
107+
}
108+
109+
// Implements Backend.
110+
func (srv *archiveService) Synced() <-chan struct{} {
111+
// Archive node is always considered synced.
112+
ch := make(chan struct{})
113+
close(ch)
114+
return ch
115+
}
116+
117+
// Implements Backend.
118+
func (srv *archiveService) EstimateGas(ctx context.Context, req *consensusAPI.EstimateGasRequest) (transaction.Gas, error) {
119+
return 0, consensusAPI.ErrUnsupported
120+
}
121+
122+
// Implements Backend.
123+
func (srv *archiveService) GetSignerNonce(ctx context.Context, req *consensusAPI.GetSignerNonceRequest) (uint64, error) {
124+
return 0, consensusAPI.ErrUnsupported
125+
}
126+
127+
// New creates a new archive-only consensus service.
128+
func NewArchive(
129+
ctx context.Context,
130+
dataDir string,
131+
identity *identity.Identity,
132+
genesisProvider genesisAPI.Provider,
133+
) (consensusAPI.Backend, error) {
134+
var err error
135+
136+
srv := &archiveService{
137+
commonNode: commonNode{
138+
BaseBackgroundService: *cmservice.NewBaseBackgroundService("tendermint"),
139+
ctx: ctx,
140+
rpcCtx: &tmrpctypes.Context{},
141+
identity: identity,
142+
dataDir: dataDir,
143+
svcMgr: cmbackground.NewServiceManager(logging.GetLogger("tendermint/servicemanager")),
144+
startedCh: make(chan struct{}),
145+
},
146+
startedCh: make(chan struct{}),
147+
quitCh: make(chan struct{}),
148+
}
149+
// Common node needs access to parent struct for initializing consensus services.
150+
srv.commonNode.parentNode = srv
151+
152+
doc, err := genesisProvider.GetGenesisDocument()
153+
if err != nil {
154+
return nil, fmt.Errorf("tendermint/archive: failed to get genesis document: %w", err)
155+
}
156+
srv.genesis = doc
157+
158+
appConfig := &abci.ApplicationConfig{
159+
DataDir: filepath.Join(srv.dataDir, tmcommon.StateDir),
160+
StorageBackend: db.GetBackendName(),
161+
Pruning: abci.PruneConfig{
162+
Strategy: abci.PruneNone,
163+
},
164+
DisableCheckpointer: true,
165+
CheckpointerCheckInterval: 100 * time.Hour, // Disabled.
166+
HaltEpochHeight: srv.genesis.HaltEpoch,
167+
OwnTxSigner: srv.identity.NodeSigner.Public(),
168+
InitialHeight: uint64(srv.genesis.Height),
169+
// ReadOnly should actually be preferable for archive but there is a badger issue with read-only:
170+
// https://discuss.dgraph.io/t/read-only-log-truncate-required-to-run-db/16444/2
171+
ReadOnlyStorage: false,
172+
}
173+
srv.mux, err = abci.NewApplicationServer(srv.ctx, nil, appConfig)
174+
if err != nil {
175+
return nil, fmt.Errorf("tendermint/archive: failed to create application server: %w", err)
176+
}
177+
178+
// Setup needed tendermint services.
179+
logger := tmcommon.NewLogAdapter(!viper.GetBool(tmcommon.CfgLogDebug))
180+
srv.abciClient = abcicli.NewLocalClient(new(tmsync.Mutex), srv.mux.Mux())
181+
182+
dbProvider, err := db.GetProvider()
183+
if err != nil {
184+
return nil, err
185+
}
186+
tmConfig := tmconfig.DefaultConfig()
187+
_ = viper.Unmarshal(&tmConfig)
188+
tmConfig.SetRoot(filepath.Join(srv.dataDir, tmcommon.StateDir))
189+
190+
// NOTE: DBContext uses a full tendermint config but the only thing that is actually used
191+
// is the data dir field.
192+
srv.blockStoreDB, err = dbProvider(&tmnode.DBContext{ID: "blockstore", Config: tmConfig})
193+
if err != nil {
194+
return nil, err
195+
}
196+
197+
// NOTE: DBContext uses a full tendermint config but the only thing that is actually used
198+
// is the data dir field.
199+
srv.stateStore, err = dbProvider(&tmnode.DBContext{ID: "state", Config: tmConfig})
200+
if err != nil {
201+
return nil, err
202+
}
203+
204+
tmGenDoc, err := api.GetTendermintGenesisDocument(genesisProvider)
205+
if err != nil {
206+
return nil, err
207+
}
208+
209+
// Setup minimal tendermint environment needed to support consensus queries.
210+
tmcore.SetEnvironment(&tmcore.Environment{
211+
ProxyAppQuery: tmproxy.NewAppConnQuery(srv.abciClient),
212+
ProxyAppMempool: nil,
213+
StateDB: srv.stateStore,
214+
BlockStore: store.NewBlockStore(srv.blockStoreDB),
215+
EvidencePool: nil,
216+
ConsensusState: nil,
217+
GenDoc: tmGenDoc,
218+
Logger: logger,
219+
Config: *tmConfig.RPC,
220+
EventBus: nil,
221+
P2PPeers: nil,
222+
P2PTransport: nil,
223+
PubKey: nil,
224+
TxIndexer: nil,
225+
ConsensusReactor: nil,
226+
Mempool: nil,
227+
})
228+
229+
return srv, srv.initialize()
230+
}

0 commit comments

Comments
 (0)