Skip to content

Commit 35393c2

Browse files
authored
Merge pull request #6235 from oasisprotocol/peternose/feature/stateless-client
go/consensus/cometbft: Add stateless client node
2 parents db5ae6b + cb10f62 commit 35393c2

33 files changed

Lines changed: 2486 additions & 179 deletions

.changelog/6235.breaking.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
go/consensus/cometbft: Switch to Protobuf encoding for block metadata
2+
3+
Block metadata now uses Protobuf encoding for the block header and the last
4+
commit. This change addresses an issue with CBOR encoding, which stripped
5+
milliseconds from timestamps, preventing light clients from verifying
6+
them.

.changelog/6235.feature.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
go/consensus/cometbft: Add stateless client node
2+
3+
A stateless client node can now be started using the following configuration.
4+
To ensure compatibility, all provider nodes specified must be running
5+
the latest version of Oasis Core.
6+
7+
```yaml
8+
mode: client-stateless
9+
# ... sections not relevant are omitted ...
10+
consensus:
11+
providers:
12+
- <node-address-1>
13+
- <node-address-2>
14+
# Add more node addresses as needed
15+
```

go/consensus/cometbft/abci/system.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import (
66
"fmt"
77

88
"github.com/cometbft/cometbft/abci/types"
9-
cmtmerkle "github.com/cometbft/cometbft/crypto/merkle"
109

1110
"github.com/oasisprotocol/oasis-core/go/common/cbor"
1211
consensus "github.com/oasisprotocol/oasis-core/go/consensus/api"
1312
"github.com/oasisprotocol/oasis-core/go/consensus/api/transaction"
1413
"github.com/oasisprotocol/oasis-core/go/consensus/cometbft/api"
1514
cmtcrypto "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/crypto"
15+
"github.com/oasisprotocol/oasis-core/go/consensus/cometbft/crypto/merkle"
1616
)
1717

1818
// prepareSystemTxs prepares a list of system transactions to be included in a proposed block in
@@ -154,5 +154,5 @@ func (mux *abciMux) computeProvableEventsRoot() ([]byte, error) {
154154
for i, pe := range provable {
155155
provableEvents[i] = cbor.Marshal(pe.ProvableRepresentation())
156156
}
157-
return cmtmerkle.HashFromByteSlices(provableEvents), nil
157+
return merkle.RootHash(provableEvents), nil
158158
}

go/consensus/cometbft/api/api.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,18 +160,25 @@ func QueryForApp(eventApp string) cmtpubsub.Query {
160160
// BlockMeta is the CometBFT-specific per-block metadata.
161161
type BlockMeta struct {
162162
// Header is the CometBFT block header.
163-
Header *cmttypes.Header `json:"header"`
163+
Header []byte `json:"header"`
164164
// LastCommit is the CometBFT last commit info.
165-
LastCommit *cmttypes.Commit `json:"last_commit"`
165+
LastCommit []byte `json:"last_commit"`
166166
}
167167

168168
// NewBlock creates a new consensus.Block from a CometBFT block.
169-
func NewBlock(blk *cmttypes.Block) *consensus.Block {
169+
func NewBlock(blk *cmttypes.Block) (*consensus.Block, error) {
170+
header, err := blk.Header.ToProto().Marshal()
171+
if err != nil {
172+
return nil, fmt.Errorf("failed to marshal header: %w", err)
173+
}
174+
lastCommit, err := blk.LastCommit.ToProto().Marshal()
175+
if err != nil {
176+
return nil, fmt.Errorf("failed to marshal last commit: %w", err)
177+
}
170178
meta := BlockMeta{
171-
Header: &blk.Header,
172-
LastCommit: blk.LastCommit,
179+
Header: header,
180+
LastCommit: lastCommit,
173181
}
174-
rawMeta := cbor.Marshal(meta)
175182

176183
var stateRoot hash.Hash
177184
switch blk.Header.AppHash {
@@ -194,8 +201,8 @@ func NewBlock(blk *cmttypes.Block) *consensus.Block {
194201
Hash: stateRoot,
195202
},
196203
Size: uint64(blk.Size()),
197-
Meta: rawMeta,
198-
}
204+
Meta: cbor.Marshal(meta),
205+
}, nil
199206
}
200207

201208
// BlockResults are CometBFT-specific consensus block results.

go/consensus/cometbft/cometbft.go

Lines changed: 164 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ package cometbft
22

33
import (
44
"context"
5+
"encoding/hex"
6+
"fmt"
7+
8+
cmtlight "github.com/cometbft/cometbft/light"
9+
cmttypes "github.com/cometbft/cometbft/types"
510

611
"github.com/oasisprotocol/oasis-core/go/common/identity"
712
"github.com/oasisprotocol/oasis-core/go/config"
813
consensusAPI "github.com/oasisprotocol/oasis-core/go/consensus/api"
914
"github.com/oasisprotocol/oasis-core/go/consensus/cometbft/api"
1015
"github.com/oasisprotocol/oasis-core/go/consensus/cometbft/full"
1116
"github.com/oasisprotocol/oasis-core/go/consensus/cometbft/light"
17+
"github.com/oasisprotocol/oasis-core/go/consensus/cometbft/stateless"
18+
"github.com/oasisprotocol/oasis-core/go/consensus/pricediscovery"
1219
genesisAPI "github.com/oasisprotocol/oasis-core/go/genesis/api"
1320
p2pAPI "github.com/oasisprotocol/oasis-core/go/p2p/api"
1421
"github.com/oasisprotocol/oasis-core/go/p2p/rpc"
@@ -30,7 +37,163 @@ func New(
3037
return nil, err
3138
}
3239

33-
commonCfg := full.CommonConfig{
40+
switch config.GlobalConfig.Mode {
41+
case config.ModeArchive:
42+
node, err := createArchiveNode(ctx, dataDir, identity, genesis, doc, genesisDoc)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to create archive node: %w", err)
45+
}
46+
return node, nil
47+
case config.ModeStatelessClient:
48+
node, err := createStatelessNode(ctx, genesis, doc, genesisDoc, p2p)
49+
if err != nil {
50+
return nil, fmt.Errorf("failed to create stateless node: %w", err)
51+
}
52+
return node, nil
53+
default:
54+
node, err := createFullNode(ctx, dataDir, identity, genesis, doc, genesisDoc, upgrader, p2p)
55+
if err != nil {
56+
return nil, fmt.Errorf("failed to create full node: %w", err)
57+
}
58+
return node, nil
59+
}
60+
}
61+
62+
func createArchiveNode(
63+
ctx context.Context,
64+
dataDir string,
65+
identity *identity.Identity,
66+
genesis genesisAPI.Provider,
67+
doc *genesisAPI.Document,
68+
genesisDoc *cmttypes.GenesisDoc,
69+
) (consensusAPI.Service, error) {
70+
cfg := full.ArchiveConfig{
71+
CommonConfig: createCommonConfig(dataDir, identity, genesis, doc, genesisDoc),
72+
}
73+
74+
return full.NewArchive(ctx, cfg)
75+
}
76+
77+
func createFullNode(
78+
ctx context.Context,
79+
dataDir string,
80+
identity *identity.Identity,
81+
genesis genesisAPI.Provider,
82+
doc *genesisAPI.Document,
83+
genesisDoc *cmttypes.GenesisDoc,
84+
upgrader upgradeAPI.Backend,
85+
p2p p2pAPI.Service,
86+
) (consensusAPI.Service, error) {
87+
cfg := full.Config{
88+
CommonConfig: createCommonConfig(dataDir, identity, genesis, doc, genesisDoc),
89+
TimeoutCommit: doc.Consensus.Parameters.TimeoutCommit,
90+
EmptyBlockInterval: doc.Consensus.Parameters.EmptyBlockInterval,
91+
SkipTimeoutCommit: doc.Consensus.Parameters.SkipTimeoutCommit,
92+
Upgrader: upgrader,
93+
}
94+
95+
return full.New(ctx, p2p, cfg)
96+
}
97+
98+
func createStatelessNode(
99+
ctx context.Context,
100+
genesis genesisAPI.Provider,
101+
doc *genesisAPI.Document,
102+
genesisDoc *cmttypes.GenesisDoc,
103+
p2p p2pAPI.Service,
104+
) (consensusAPI.Service, error) {
105+
provider, err := createProvider()
106+
if err != nil {
107+
return nil, fmt.Errorf("failed to create provider: %w", err)
108+
}
109+
110+
lightClient, err := createLightClient(ctx, genesisDoc, doc, p2p)
111+
if err != nil {
112+
return nil, fmt.Errorf("failed to create light client: %w", err)
113+
}
114+
115+
services, err := createStatelessServices(doc, genesis, genesisDoc, provider, lightClient)
116+
if err != nil {
117+
return nil, fmt.Errorf("failed to create stateless client: %w", err)
118+
}
119+
120+
submitter, err := createSubmissionManager(ctx, services)
121+
if err != nil {
122+
return nil, fmt.Errorf("failed to create submission manager: %w", err)
123+
}
124+
125+
return stateless.NewService(services, submitter)
126+
}
127+
128+
func createStatelessServices(
129+
doc *genesisAPI.Document,
130+
genesis genesisAPI.Provider,
131+
genesisDoc *cmttypes.GenesisDoc,
132+
provider *consensusAPI.Client,
133+
lightClient *light.Client,
134+
) (*stateless.Services, error) {
135+
cfg := stateless.Config{
136+
ChainID: doc.ChainID,
137+
ChainContext: doc.ChainContext(),
138+
Genesis: genesis,
139+
GenesisDoc: genesisDoc,
140+
GenesisHeight: doc.Height,
141+
BaseEpoch: doc.Beacon.Base,
142+
BaseHeight: doc.Height,
143+
}
144+
145+
return stateless.NewServices(provider, lightClient, cfg)
146+
}
147+
148+
func createProvider() (*consensusAPI.Client, error) {
149+
addresses := config.GlobalConfig.Consensus.Providers
150+
if len(addresses) == 0 {
151+
return nil, fmt.Errorf("no providers configured")
152+
}
153+
154+
return stateless.NewProvider(addresses[0])
155+
}
156+
157+
func createLightClient(
158+
ctx context.Context,
159+
genesisDoc *cmttypes.GenesisDoc,
160+
doc *genesisAPI.Document,
161+
p2p p2pAPI.Service,
162+
) (*light.Client, error) {
163+
hash, err := hex.DecodeString(config.GlobalConfig.Consensus.LightClient.Trust.Hash)
164+
if err != nil {
165+
return nil, fmt.Errorf("failed to decode trust hash: %w", err)
166+
}
167+
168+
cfg := light.Config{
169+
GenesisDocument: genesisDoc,
170+
TrustOptions: cmtlight.TrustOptions{
171+
Period: config.GlobalConfig.Consensus.LightClient.Trust.Period,
172+
Height: int64(config.GlobalConfig.Consensus.LightClient.Trust.Height),
173+
Hash: hash,
174+
},
175+
}
176+
177+
return light.NewClient(ctx, doc.ChainContext(), p2p, cfg)
178+
}
179+
180+
func createSubmissionManager(ctx context.Context, services consensusAPI.Services) (consensusAPI.SubmissionManager, error) {
181+
pd, err := pricediscovery.New(ctx, services.Core(), config.GlobalConfig.Consensus.Submission.GasPrice)
182+
if err != nil {
183+
return nil, fmt.Errorf("failed to create price discovery: %w", err)
184+
}
185+
186+
return consensusAPI.NewSubmissionManager(services, pd, config.GlobalConfig.Consensus.Submission.MaxFee), nil
187+
}
188+
189+
func createCommonConfig(
190+
dataDir string,
191+
identity *identity.Identity,
192+
genesis genesisAPI.Provider,
193+
doc *genesisAPI.Document,
194+
genesisDoc *cmttypes.GenesisDoc,
195+
) full.CommonConfig {
196+
return full.CommonConfig{
34197
DataDir: dataDir,
35198
Identity: identity,
36199
ChainID: doc.ChainID,
@@ -42,23 +205,6 @@ func New(
42205
BaseHeight: doc.Height,
43206
PublicKeyBlacklist: doc.Consensus.Parameters.PublicKeyBlacklist,
44207
}
45-
46-
switch config.GlobalConfig.Mode {
47-
case config.ModeArchive:
48-
cfg := full.ArchiveConfig{
49-
CommonConfig: commonCfg,
50-
}
51-
return full.NewArchive(ctx, cfg)
52-
default:
53-
cfg := full.Config{
54-
CommonConfig: commonCfg,
55-
TimeoutCommit: doc.Consensus.Parameters.TimeoutCommit,
56-
EmptyBlockInterval: doc.Consensus.Parameters.EmptyBlockInterval,
57-
SkipTimeoutCommit: doc.Consensus.Parameters.SkipTimeoutCommit,
58-
Upgrader: upgrader,
59-
}
60-
return full.New(ctx, p2p, cfg)
61-
}
62208
}
63209

64210
// NewLightService creates a new CometBFT light client service.

go/consensus/cometbft/config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ type Config struct {
5151
// LightClient contains light client configuration.
5252
LightClient LightClientConfig `yaml:"light_client,omitempty"`
5353

54+
// Providers contains the CometBFT node addresses the stateless client
55+
// connects to in order to fetch consensus-related data.
56+
Providers []string `yaml:"providers,omitempty"`
57+
5458
// Supplementary sanity checks configuration.
5559
SupplementarySanity SupplementarySanityConfig `yaml:"supplementary_sanity,omitempty"`
5660

0 commit comments

Comments
 (0)