2023-09-25 02:43:35 +00:00
|
|
|
package ceremony
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2023-10-05 07:05:02 +00:00
|
|
|
"context"
|
2023-09-25 02:43:35 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"go.uber.org/zap"
|
2023-10-26 05:55:39 +00:00
|
|
|
"go.uber.org/zap/zapcore"
|
2023-10-05 07:05:02 +00:00
|
|
|
"google.golang.org/grpc"
|
2023-09-25 02:43:35 +00:00
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"google.golang.org/protobuf/types/known/anypb"
|
2024-02-13 07:04:56 +00:00
|
|
|
"source.quilibrium.com/quilibrium/monorepo/node/execution/intrinsics/ceremony/application"
|
2023-10-05 07:05:02 +00:00
|
|
|
"source.quilibrium.com/quilibrium/monorepo/node/p2p"
|
2023-09-25 02:43:35 +00:00
|
|
|
"source.quilibrium.com/quilibrium/monorepo/node/protobufs"
|
|
|
|
"source.quilibrium.com/quilibrium/monorepo/node/store"
|
|
|
|
)
|
|
|
|
|
2023-10-26 05:55:39 +00:00
|
|
|
var ErrNoNewFrames = errors.New("peer reported no frames")
|
|
|
|
|
2023-10-05 07:05:02 +00:00
|
|
|
// GetCompressedSyncFrames implements protobufs.CeremonyServiceServer.
|
|
|
|
func (e *CeremonyDataClockConsensusEngine) GetCompressedSyncFrames(
|
|
|
|
request *protobufs.ClockFramesRequest,
|
|
|
|
server protobufs.CeremonyService_GetCompressedSyncFramesServer,
|
|
|
|
) error {
|
|
|
|
e.logger.Info(
|
|
|
|
"received clock frame request",
|
|
|
|
zap.Uint64("from_frame_number", request.FromFrameNumber),
|
|
|
|
zap.Uint64("to_frame_number", request.ToFrameNumber),
|
|
|
|
)
|
|
|
|
|
2024-02-20 20:01:10 +00:00
|
|
|
if e.currentReceivingSyncPeers > 4 {
|
|
|
|
e.logger.Info(
|
|
|
|
"currently processing maximum sync requests, returning",
|
|
|
|
)
|
|
|
|
|
|
|
|
if err := server.SendMsg(
|
|
|
|
&protobufs.ClockFramesResponse{
|
|
|
|
Filter: request.Filter,
|
|
|
|
FromFrameNumber: 0,
|
|
|
|
ToFrameNumber: 0,
|
|
|
|
ClockFrames: []*protobufs.ClockFrame{},
|
|
|
|
},
|
|
|
|
); err != nil {
|
|
|
|
return errors.Wrap(err, "get compressed sync frames")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
e.currentReceivingSyncPeers++
|
|
|
|
defer func() { e.currentReceivingSyncPeers-- }()
|
|
|
|
|
2023-10-05 07:05:02 +00:00
|
|
|
from := request.FromFrameNumber
|
2023-11-01 03:45:20 +00:00
|
|
|
parent := request.ParentSelector
|
2023-10-05 07:05:02 +00:00
|
|
|
|
2023-10-14 04:05:44 +00:00
|
|
|
frame, _, err := e.clockStore.GetDataClockFrame(
|
2023-10-05 07:05:02 +00:00
|
|
|
request.Filter,
|
|
|
|
from,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
if !errors.Is(err, store.ErrNotFound) {
|
|
|
|
e.logger.Error(
|
|
|
|
"peer asked for frame that returned error",
|
|
|
|
zap.Uint64("frame_number", request.FromFrameNumber),
|
|
|
|
)
|
|
|
|
return errors.Wrap(err, "get compressed sync frames")
|
|
|
|
} else {
|
2023-11-01 03:45:20 +00:00
|
|
|
frames, err := e.clockStore.GetCandidateDataClockFrames(e.filter, from)
|
|
|
|
if err != nil || len(frames) == 0 {
|
|
|
|
e.logger.Debug(
|
|
|
|
"peer asked for undiscovered frame",
|
|
|
|
zap.Uint64("frame_number", request.FromFrameNumber),
|
|
|
|
)
|
|
|
|
|
|
|
|
if err := server.SendMsg(
|
|
|
|
&protobufs.ClockFramesResponse{
|
|
|
|
Filter: request.Filter,
|
|
|
|
FromFrameNumber: 0,
|
|
|
|
ToFrameNumber: 0,
|
|
|
|
ClockFrames: []*protobufs.ClockFrame{},
|
|
|
|
},
|
|
|
|
); err != nil {
|
|
|
|
return errors.Wrap(err, "get compressed sync frames")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2023-10-05 07:05:02 +00:00
|
|
|
}
|
|
|
|
|
2023-11-01 03:45:20 +00:00
|
|
|
parent = nil
|
2023-10-05 07:05:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-14 04:05:44 +00:00
|
|
|
if parent != nil {
|
|
|
|
if !bytes.Equal(frame.ParentSelector, parent) {
|
2024-02-20 07:59:03 +00:00
|
|
|
e.logger.Debug(
|
2023-10-14 04:05:44 +00:00
|
|
|
"peer specified out of consensus head, seeking backwards for fork",
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
for !bytes.Equal(frame.ParentSelector, parent) {
|
|
|
|
ours, err := e.clockStore.GetParentDataClockFrame(
|
|
|
|
e.filter,
|
|
|
|
frame.FrameNumber-1,
|
|
|
|
frame.ParentSelector,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
from = 1
|
2024-01-10 06:58:38 +00:00
|
|
|
e.logger.Debug("peer fully out of sync, rewinding sync head to start")
|
2023-10-14 04:05:44 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
theirs, err := e.clockStore.GetParentDataClockFrame(
|
|
|
|
e.filter,
|
|
|
|
frame.FrameNumber-1,
|
|
|
|
parent,
|
|
|
|
)
|
|
|
|
if err != nil {
|
2024-02-20 07:59:03 +00:00
|
|
|
if frame.FrameNumber > 18 {
|
|
|
|
from = frame.FrameNumber - 16
|
|
|
|
} else {
|
|
|
|
from = 2
|
|
|
|
}
|
2024-01-10 06:58:38 +00:00
|
|
|
e.logger.Debug("peer fully out of sync, rewinding sync head to min")
|
2023-10-14 04:05:44 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
from--
|
|
|
|
frame = ours
|
|
|
|
parent = theirs.ParentSelector
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-13 07:04:56 +00:00
|
|
|
head, err := e.dataTimeReel.Head()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
max := head.FrameNumber
|
2023-10-05 07:05:02 +00:00
|
|
|
to := request.ToFrameNumber
|
|
|
|
|
2023-11-01 03:45:20 +00:00
|
|
|
// We need to slightly rewind, to compensate for unconfirmed frame heads on a
|
|
|
|
// given branch
|
|
|
|
if from >= 2 {
|
|
|
|
from--
|
|
|
|
}
|
|
|
|
|
2023-10-05 07:05:02 +00:00
|
|
|
for {
|
2024-02-20 07:59:03 +00:00
|
|
|
if to == 0 || to-from > 16 {
|
|
|
|
if max > from+15 {
|
|
|
|
to = from + 16
|
2023-10-05 07:05:02 +00:00
|
|
|
} else {
|
2023-10-26 05:55:39 +00:00
|
|
|
to = max + 1
|
2023-10-05 07:05:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
syncMsg, err := e.clockStore.GetCompressedDataClockFrames(
|
|
|
|
e.filter,
|
|
|
|
from,
|
|
|
|
to,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "get compressed sync frames")
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := server.SendMsg(syncMsg); err != nil {
|
|
|
|
return errors.Wrap(err, "get compressed sync frames")
|
|
|
|
}
|
|
|
|
|
|
|
|
if (request.ToFrameNumber == 0 || request.ToFrameNumber > to) && max > to {
|
|
|
|
from = to + 1
|
|
|
|
if request.ToFrameNumber > to {
|
|
|
|
to = request.ToFrameNumber
|
|
|
|
} else {
|
|
|
|
to = 0
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *CeremonyDataClockConsensusEngine) decompressAndStoreCandidates(
|
2023-11-01 03:45:20 +00:00
|
|
|
peerId []byte,
|
2023-10-05 07:05:02 +00:00
|
|
|
syncMsg *protobufs.CeremonyCompressedSync,
|
2023-10-26 05:55:39 +00:00
|
|
|
loggerFunc func(msg string, fields ...zapcore.Field),
|
|
|
|
) (*protobufs.ClockFrame, error) {
|
|
|
|
if len(syncMsg.TruncatedClockFrames) == 0 {
|
|
|
|
return nil, ErrNoNewFrames
|
|
|
|
}
|
|
|
|
|
2023-11-01 03:45:20 +00:00
|
|
|
if len(syncMsg.TruncatedClockFrames) < int(
|
2023-10-14 04:05:44 +00:00
|
|
|
syncMsg.ToFrameNumber-syncMsg.FromFrameNumber+1,
|
|
|
|
) {
|
2023-11-01 03:45:20 +00:00
|
|
|
e.peerMapMx.Lock()
|
2023-11-08 06:27:06 +00:00
|
|
|
if _, ok := e.peerMap[string(peerId)]; ok {
|
|
|
|
e.uncooperativePeersMap[string(peerId)] = e.peerMap[string(peerId)]
|
2024-01-10 06:58:38 +00:00
|
|
|
e.uncooperativePeersMap[string(peerId)].timestamp = time.Now().UnixMilli()
|
2023-11-08 06:27:06 +00:00
|
|
|
delete(e.peerMap, string(peerId))
|
|
|
|
}
|
2023-11-01 03:45:20 +00:00
|
|
|
e.peerMapMx.Unlock()
|
2023-10-26 05:55:39 +00:00
|
|
|
return nil, errors.New("invalid continuity for compressed sync response")
|
2023-10-14 04:05:44 +00:00
|
|
|
}
|
|
|
|
|
2023-10-26 05:55:39 +00:00
|
|
|
var final *protobufs.ClockFrame
|
2023-10-05 07:05:02 +00:00
|
|
|
for _, frame := range syncMsg.TruncatedClockFrames {
|
2023-10-14 04:05:44 +00:00
|
|
|
frame := frame
|
2023-10-05 07:05:02 +00:00
|
|
|
commits := (len(frame.Input) - 516) / 74
|
2023-10-26 05:55:39 +00:00
|
|
|
loggerFunc(
|
2023-10-05 07:05:02 +00:00
|
|
|
"processing frame",
|
|
|
|
zap.Uint64("frame_number", frame.FrameNumber),
|
|
|
|
zap.Int("aggregate_commits", commits),
|
|
|
|
)
|
|
|
|
for j := 0; j < commits; j++ {
|
2023-10-26 05:55:39 +00:00
|
|
|
loggerFunc(
|
2023-10-05 07:05:02 +00:00
|
|
|
"processing commit",
|
|
|
|
zap.Uint64("frame_number", frame.FrameNumber),
|
|
|
|
zap.Int("commit_index", j),
|
|
|
|
)
|
|
|
|
commit := frame.Input[516+(j*74) : 516+((j+1)*74)]
|
|
|
|
var aggregateProof *protobufs.InclusionProofsMap
|
|
|
|
for _, a := range syncMsg.Proofs {
|
2023-10-14 04:05:44 +00:00
|
|
|
a := a
|
2023-10-05 07:05:02 +00:00
|
|
|
if bytes.Equal(a.FrameCommit, commit) {
|
2023-10-26 05:55:39 +00:00
|
|
|
loggerFunc(
|
2023-10-05 07:05:02 +00:00
|
|
|
"found matching proof",
|
|
|
|
zap.Uint64("frame_number", frame.FrameNumber),
|
|
|
|
zap.Int("commit_index", j),
|
|
|
|
)
|
|
|
|
aggregateProof = a
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if aggregateProof == nil {
|
|
|
|
e.logger.Error(
|
|
|
|
"could not find matching proof",
|
|
|
|
zap.Uint64("frame_number", frame.FrameNumber),
|
|
|
|
zap.Int("commit_index", j),
|
|
|
|
zap.Binary("proof", aggregateProof.Proof),
|
|
|
|
)
|
2023-10-26 05:55:39 +00:00
|
|
|
return nil, errors.Wrap(
|
2023-10-05 07:05:02 +00:00
|
|
|
store.ErrInvalidData,
|
|
|
|
"decompress and store candidates",
|
|
|
|
)
|
|
|
|
}
|
|
|
|
inc := &protobufs.InclusionAggregateProof{
|
|
|
|
Filter: e.filter,
|
|
|
|
FrameNumber: frame.FrameNumber,
|
|
|
|
InclusionCommitments: []*protobufs.InclusionCommitment{},
|
|
|
|
Proof: aggregateProof.Proof,
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, c := range aggregateProof.Commitments {
|
2023-10-14 04:05:44 +00:00
|
|
|
k := k
|
|
|
|
c := c
|
2023-10-26 05:55:39 +00:00
|
|
|
loggerFunc(
|
2023-10-05 07:05:02 +00:00
|
|
|
"adding inclusion commitment",
|
|
|
|
zap.Uint64("frame_number", frame.FrameNumber),
|
|
|
|
zap.Int("commit_index", j),
|
|
|
|
zap.Int("inclusion_commit_index", k),
|
|
|
|
zap.String("type_url", c.TypeUrl),
|
|
|
|
)
|
|
|
|
incCommit := &protobufs.InclusionCommitment{
|
|
|
|
Filter: e.filter,
|
|
|
|
FrameNumber: frame.FrameNumber,
|
|
|
|
Position: uint32(k),
|
|
|
|
TypeUrl: c.TypeUrl,
|
|
|
|
Data: []byte{},
|
|
|
|
Commitment: c.Commitment,
|
|
|
|
}
|
|
|
|
var output *protobufs.IntrinsicExecutionOutput
|
|
|
|
if c.TypeUrl == protobufs.IntrinsicExecutionOutputType {
|
|
|
|
output = &protobufs.IntrinsicExecutionOutput{}
|
|
|
|
}
|
|
|
|
for l, h := range c.SegmentHashes {
|
2023-10-14 04:05:44 +00:00
|
|
|
l := l
|
|
|
|
h := h
|
|
|
|
|
2023-10-05 07:05:02 +00:00
|
|
|
for _, s := range syncMsg.Segments {
|
2023-10-14 04:05:44 +00:00
|
|
|
s := s
|
|
|
|
|
2023-10-05 07:05:02 +00:00
|
|
|
if bytes.Equal(s.Hash, h) {
|
|
|
|
if output != nil {
|
|
|
|
if l == 0 {
|
2023-10-26 05:55:39 +00:00
|
|
|
loggerFunc(
|
2023-10-05 07:05:02 +00:00
|
|
|
"found first half of matching segment data",
|
|
|
|
zap.Uint64("frame_number", frame.FrameNumber),
|
|
|
|
zap.Int("commit_index", j),
|
|
|
|
zap.Int("inclusion_commit_index", k),
|
|
|
|
zap.String("type_url", c.TypeUrl),
|
|
|
|
)
|
|
|
|
output.Address = s.Data[:32]
|
|
|
|
output.Output = s.Data[32:]
|
|
|
|
} else {
|
2023-10-26 05:55:39 +00:00
|
|
|
loggerFunc(
|
2023-10-05 07:05:02 +00:00
|
|
|
"found second half of matching segment data",
|
|
|
|
zap.Uint64("frame_number", frame.FrameNumber),
|
|
|
|
zap.Int("commit_index", j),
|
|
|
|
zap.Int("inclusion_commit_index", k),
|
|
|
|
zap.String("type_url", c.TypeUrl),
|
|
|
|
)
|
|
|
|
output.Proof = s.Data
|
|
|
|
b, err := proto.Marshal(output)
|
|
|
|
if err != nil {
|
2023-10-26 05:55:39 +00:00
|
|
|
return nil, errors.Wrap(
|
|
|
|
err,
|
|
|
|
"decompress and store candidates",
|
|
|
|
)
|
2023-10-05 07:05:02 +00:00
|
|
|
}
|
|
|
|
incCommit.Data = b
|
|
|
|
break
|
|
|
|
}
|
|
|
|
} else {
|
2023-10-26 05:55:39 +00:00
|
|
|
loggerFunc(
|
2023-10-05 07:05:02 +00:00
|
|
|
"found matching segment data",
|
|
|
|
zap.Uint64("frame_number", frame.FrameNumber),
|
|
|
|
zap.Int("commit_index", j),
|
|
|
|
zap.Int("inclusion_commit_index", k),
|
|
|
|
zap.String("type_url", c.TypeUrl),
|
|
|
|
)
|
|
|
|
incCommit.Data = append(incCommit.Data, s.Data...)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
inc.InclusionCommitments = append(
|
|
|
|
inc.InclusionCommitments,
|
|
|
|
incCommit,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
frame.AggregateProofs = append(
|
|
|
|
frame.AggregateProofs,
|
|
|
|
inc,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
f, err := proto.Marshal(frame)
|
|
|
|
if err != nil {
|
2023-10-26 05:55:39 +00:00
|
|
|
return nil, errors.Wrap(err, "decompress and store candidates")
|
2023-10-05 07:05:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
any := &anypb.Any{
|
|
|
|
TypeUrl: protobufs.ClockFrameType,
|
|
|
|
Value: f,
|
|
|
|
}
|
|
|
|
if err = e.handleClockFrameData(
|
|
|
|
e.syncingTarget,
|
2024-01-03 07:49:12 +00:00
|
|
|
append(
|
|
|
|
p2p.GetBloomFilter(application.CEREMONY_ADDRESS, 256, 3),
|
|
|
|
p2p.GetBloomFilterIndices(application.CEREMONY_ADDRESS, 65536, 24)...,
|
|
|
|
),
|
2023-10-05 07:05:02 +00:00
|
|
|
any,
|
|
|
|
true,
|
|
|
|
); err != nil {
|
2023-10-26 05:55:39 +00:00
|
|
|
return nil, errors.Wrap(err, "decompress and store candidates")
|
2023-10-05 07:05:02 +00:00
|
|
|
}
|
2023-10-26 05:55:39 +00:00
|
|
|
final = frame
|
2023-10-05 07:05:02 +00:00
|
|
|
}
|
|
|
|
|
2023-10-26 05:55:39 +00:00
|
|
|
loggerFunc(
|
2023-10-05 07:05:02 +00:00
|
|
|
"decompressed and stored sync for range",
|
|
|
|
zap.Uint64("from", syncMsg.FromFrameNumber),
|
|
|
|
zap.Uint64("to", syncMsg.ToFrameNumber),
|
|
|
|
)
|
2023-10-26 05:55:39 +00:00
|
|
|
return final, nil
|
2023-10-05 07:05:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type svr struct {
|
|
|
|
protobufs.UnimplementedCeremonyServiceServer
|
|
|
|
svrChan chan protobufs.CeremonyService_GetPublicChannelServer
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *svr) GetCompressedSyncFrames(
|
|
|
|
request *protobufs.ClockFramesRequest,
|
|
|
|
server protobufs.CeremonyService_GetCompressedSyncFramesServer,
|
|
|
|
) error {
|
|
|
|
return errors.New("not supported")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *svr) GetPublicChannel(
|
|
|
|
server protobufs.CeremonyService_GetPublicChannelServer,
|
|
|
|
) error {
|
|
|
|
go func() {
|
|
|
|
e.svrChan <- server
|
|
|
|
}()
|
|
|
|
<-server.Context().Done()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *CeremonyDataClockConsensusEngine) GetPublicChannelForProvingKey(
|
|
|
|
initiator bool,
|
|
|
|
provingKey []byte,
|
|
|
|
) (p2p.PublicChannelClient, error) {
|
|
|
|
if initiator {
|
|
|
|
svrChan := make(
|
|
|
|
chan protobufs.CeremonyService_GetPublicChannelServer,
|
|
|
|
)
|
|
|
|
after := time.After(20 * time.Second)
|
|
|
|
go func() {
|
|
|
|
server := grpc.NewServer(
|
2023-10-27 00:19:19 +00:00
|
|
|
grpc.MaxSendMsgSize(600*1024*1024),
|
|
|
|
grpc.MaxRecvMsgSize(600*1024*1024),
|
2023-10-05 07:05:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
s := &svr{
|
|
|
|
svrChan: svrChan,
|
|
|
|
}
|
|
|
|
protobufs.RegisterCeremonyServiceServer(server, s)
|
|
|
|
|
|
|
|
if err := e.pubSub.StartDirectChannelListener(
|
|
|
|
provingKey,
|
|
|
|
server,
|
|
|
|
); err != nil {
|
|
|
|
e.logger.Error(
|
|
|
|
"could not get public channel for proving key",
|
|
|
|
zap.Error(err),
|
|
|
|
)
|
|
|
|
svrChan <- nil
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
select {
|
|
|
|
case s := <-svrChan:
|
|
|
|
return s, nil
|
|
|
|
case <-after:
|
|
|
|
return nil, errors.Wrap(
|
|
|
|
errors.New("timed out"),
|
|
|
|
"get public channel for proving key",
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
cc, err := e.pubSub.GetDirectChannel(provingKey)
|
|
|
|
if err != nil {
|
|
|
|
e.logger.Error(
|
|
|
|
"could not get public channel for proving key",
|
|
|
|
zap.Error(err),
|
|
|
|
)
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
client := protobufs.NewCeremonyServiceClient(cc)
|
|
|
|
s, err := client.GetPublicChannel(
|
|
|
|
context.Background(),
|
2023-10-27 00:19:19 +00:00
|
|
|
grpc.MaxCallSendMsgSize(600*1024*1024),
|
|
|
|
grpc.MaxCallRecvMsgSize(600*1024*1024),
|
2023-10-05 07:05:02 +00:00
|
|
|
)
|
2023-10-07 02:44:35 +00:00
|
|
|
return s, errors.Wrap(err, "get public channel for proving key")
|
2023-10-05 07:05:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetPublicChannel implements protobufs.CeremonyServiceServer.
|
|
|
|
func (e *CeremonyDataClockConsensusEngine) GetPublicChannel(
|
|
|
|
server protobufs.CeremonyService_GetPublicChannelServer,
|
|
|
|
) error {
|
|
|
|
return errors.New("not supported")
|
|
|
|
}
|