package ceremony

import (
	"bytes"
	"crypto"
	"crypto/rand"
	_ "embed"
	"encoding/base64"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"strings"
	"sync"

	"github.com/iden3/go-iden3-crypto/poseidon"
	"github.com/pkg/errors"
	"go.uber.org/zap"
	"golang.org/x/crypto/sha3"
	"golang.org/x/sync/errgroup"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/anypb"

	"source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves"
	"source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/vdf"
	"source.quilibrium.com/quilibrium/monorepo/node/config"
	"source.quilibrium.com/quilibrium/monorepo/node/consensus/ceremony"
	"source.quilibrium.com/quilibrium/monorepo/node/consensus/time"
	qcrypto "source.quilibrium.com/quilibrium/monorepo/node/crypto"
	"source.quilibrium.com/quilibrium/monorepo/node/crypto/kzg"
	"source.quilibrium.com/quilibrium/monorepo/node/execution"
	"source.quilibrium.com/quilibrium/monorepo/node/execution/intrinsics/ceremony/application"
	"source.quilibrium.com/quilibrium/monorepo/node/keys"
	"source.quilibrium.com/quilibrium/monorepo/node/p2p"
	"source.quilibrium.com/quilibrium/monorepo/node/protobufs"
	"source.quilibrium.com/quilibrium/monorepo/node/store"
	"source.quilibrium.com/quilibrium/monorepo/node/tries"
)

type CeremonyExecutionEngine struct {
	logger                     *zap.Logger
	clock                      *ceremony.CeremonyDataClockConsensusEngine
	clockStore                 store.ClockStore
	keyStore                   store.KeyStore
	keyManager                 keys.KeyManager
	engineConfig               *config.EngineConfig
	pubSub                     p2p.PubSub
	peerIdHash                 []byte
	provingKey                 crypto.Signer
	proverPublicKey            []byte
	provingKeyAddress          []byte
	inclusionProver            qcrypto.InclusionProver
	participantMx              sync.Mutex
	peerChannels               map[string]*p2p.PublicP2PChannel
	activeSecrets              []curves.Scalar
	activeClockFrame           *protobufs.ClockFrame
	alreadyPublishedShare      bool
	alreadyPublishedTranscript bool
	seenMessageMap             map[string]bool
	seenMessageMx              sync.Mutex
	intrinsicFilter            []byte
	frameProver                qcrypto.FrameProver
}

const validCeremonySelector = "253f3a6383dcfe91cf49abd20204b3e6ef5afd4c70c1968bb1f0b827a72af53b"

func NewCeremonyExecutionEngine(
	logger *zap.Logger,
	engineConfig *config.EngineConfig,
	keyManager keys.KeyManager,
	pubSub p2p.PubSub,
	frameProver qcrypto.FrameProver,
	inclusionProver qcrypto.InclusionProver,
	clockStore store.ClockStore,
	masterTimeReel *time.MasterTimeReel,
	peerInfoManager p2p.PeerInfoManager,
	keyStore store.KeyStore,
) *CeremonyExecutionEngine {
	if logger == nil {
		panic(errors.New("logger is nil"))
	}

	seed, err := hex.DecodeString(engineConfig.GenesisSeed)
	if err != nil {
		panic(err)
	}

	intrinsicFilter := append(
		p2p.GetBloomFilter(application.CEREMONY_ADDRESS, 256, 3),
		p2p.GetBloomFilterIndices(application.CEREMONY_ADDRESS, 65536, 24)...,
	)

	frame, _, err := clockStore.GetDataClockFrame(intrinsicFilter, 0, false)
	var origin []byte
	var inclusionProof *qcrypto.InclusionAggregateProof
	var proverKeys [][]byte

	rebuildGenesisFrame := false
	if frame != nil {
		selector, err := frame.GetSelector()
		if err != nil {
			panic(err)
		}

		if selector.Text(16) != validCeremonySelector {
			logger.Warn("corrupted genesis frame detected, rebuilding")

			err = clockStore.ResetDataClockFrames(intrinsicFilter)
			if err != nil {
				panic(err)
			}

			rebuildGenesisFrame = true
		}
	}

	if err != nil && errors.Is(err, store.ErrNotFound) || rebuildGenesisFrame {
		origin, inclusionProof, proverKeys = CreateGenesisState(
			logger,
			engineConfig,
			nil,
			inclusionProver,
		)
	}

	dataTimeReel := time.NewDataTimeReel(
		intrinsicFilter,
		logger,
		clockStore,
		engineConfig,
		frameProver,
		origin,
		inclusionProof,
		proverKeys,
	)

	clock := ceremony.NewCeremonyDataClockConsensusEngine(
		engineConfig,
		logger,
		keyManager,
		clockStore,
		keyStore,
		pubSub,
		frameProver,
		inclusionProver,
		masterTimeReel,
		dataTimeReel,
		peerInfoManager,
		intrinsicFilter,
		seed,
	)

	e := &CeremonyExecutionEngine{
		logger:                logger,
		clock:                 clock,
		engineConfig:          engineConfig,
		keyManager:            keyManager,
		clockStore:            clockStore,
		keyStore:              keyStore,
		pubSub:                pubSub,
		inclusionProver:       inclusionProver,
		frameProver:           frameProver,
		participantMx:         sync.Mutex{},
		peerChannels:          map[string]*p2p.PublicP2PChannel{},
		alreadyPublishedShare: false,
		seenMessageMx:         sync.Mutex{},
		seenMessageMap:        map[string]bool{},
		intrinsicFilter:       intrinsicFilter,
	}

	peerId := e.pubSub.GetPeerID()
	addr, err := poseidon.HashBytes(peerId)
	if err != nil {
		panic(err)
	}

	addrBytes := addr.Bytes()
	addrBytes = append(make([]byte, 32-len(addrBytes)), addrBytes...)
	e.peerIdHash = addrBytes
	provingKey, _, publicKeyBytes, provingKeyAddress := e.clock.GetProvingKey(
		engineConfig,
	)
	e.provingKey = provingKey
	e.proverPublicKey = publicKeyBytes
	e.provingKeyAddress = provingKeyAddress

	return e
}

var _ execution.ExecutionEngine = (*CeremonyExecutionEngine)(nil)

// GetName implements ExecutionEngine
func (*CeremonyExecutionEngine) GetName() string {
	return "ceremony"
}

// GetSupportedApplications implements ExecutionEngine
func (
	*CeremonyExecutionEngine,
) GetSupportedApplications() []*protobufs.Application {
	return []*protobufs.Application{
		{
			Address:          application.CEREMONY_ADDRESS,
			ExecutionContext: protobufs.ExecutionContext_EXECUTION_CONTEXT_INTRINSIC,
		},
	}
}

// 2024-01-03: 1.2.0
//
//go:embed retroactive_peers.json
var retroactivePeersJsonBinary []byte

// Creates a genesis state for the intrinsic
func CreateGenesisState(
	logger *zap.Logger,
	engineConfig *config.EngineConfig,
	testProverKeys [][]byte,
	inclusionProver qcrypto.InclusionProver,
) (
	[]byte,
	*qcrypto.InclusionAggregateProof,
	[][]byte,
) {
	seed, err := hex.DecodeString(engineConfig.GenesisSeed)

	if err != nil {
		panic(errors.New("genesis seed is nil"))
	}

	logger.Info("creating genesis frame")
	for _, l := range strings.Split(string(seed), "\n") {
		logger.Info(l)
	}

	difficulty := engineConfig.Difficulty
	if difficulty == 0 {
		difficulty = 10000
	}

	b := sha3.Sum256(seed)
	v := vdf.New(difficulty, b)

	v.Execute()
	o := v.GetOutput()
	inputMessage := o[:]

	// Signatories are special, they don't have an inclusion proof because they
	// have not broadcasted communication keys, but they still get contribution
	// rights prior to PoMW, because they did produce meaningful work in the
	// first phase:
	logger.Info("encoding signatories to prover trie")
	proverKeys := [][]byte{}
	if len(testProverKeys) != 0 {
		logger.Warn(
			"TEST PROVER ENTRIES BEING ADDED, YOUR NODE WILL BE KICKED IF IN" +
				" PRODUCTION",
		)
		proverKeys = testProverKeys
	} else {
		for _, s := range kzg.CeremonySignatories {
			pubkey := s.ToAffineCompressed()
			logger.Info("0x" + hex.EncodeToString(pubkey))

			proverKeys = append(proverKeys, pubkey)
		}
	}

	logger.Info("encoding ceremony and phase one signatories")
	transcript := &protobufs.CeremonyTranscript{}
	for p, s := range kzg.CeremonyBLS48581G1 {
		transcript.G1Powers = append(
			transcript.G1Powers,
			&protobufs.BLS48581G1PublicKey{
				KeyValue: s.ToAffineCompressed(),
			},
		)
		logger.Info(fmt.Sprintf("encoded G1 power %d", p))
	}
	for p, s := range kzg.CeremonyBLS48581G2 {
		transcript.G2Powers = append(
			transcript.G2Powers,
			&protobufs.BLS48581G2PublicKey{
				KeyValue: s.ToAffineCompressed(),
			},
		)
		logger.Info(fmt.Sprintf("encoded G2 power %d", p))
	}

	transcript.RunningG1_256Witnesses = append(
		transcript.RunningG1_256Witnesses,
		&protobufs.BLS48581G1PublicKey{
			KeyValue: kzg.CeremonyRunningProducts[0].ToAffineCompressed(),
		},
	)

	transcript.RunningG2_256Powers = append(
		transcript.RunningG2_256Powers,
		&protobufs.BLS48581G2PublicKey{
			KeyValue: kzg.CeremonyBLS48581G2[len(kzg.CeremonyBLS48581G2)-1].
				ToAffineCompressed(),
		},
	)

	outputProof := &protobufs.CeremonyLobbyStateTransition{
		TypeUrls:         []string{},
		TransitionInputs: [][]byte{},
	}

	proofBytes, err := proto.Marshal(outputProof)
	if err != nil {
		panic(err)
	}

	logger.Info("encoded transcript")
	logger.Info("encoding ceremony signatories into application state")

	rewardTrie := &tries.RewardCritbitTrie{}
	for _, s := range kzg.CeremonySignatories {
		pubkey := s.ToAffineCompressed()

		addr, err := poseidon.HashBytes(pubkey)
		if err != nil {
			panic(err)
		}

		addrBytes := addr.Bytes()
		addrBytes = append(make([]byte, 32-len(addrBytes)), addrBytes...)
		rewardTrie.Add(addrBytes, 0, 50)
	}

	type peerData struct {
		PeerId       string `json:"peer_id"`
		TokenBalance uint64 `json:"token_balance"`
	}
	type rewards struct {
		Rewards []peerData `json:"rewards"`
	}

	retroEntries := &rewards{}
	err = json.Unmarshal(retroactivePeersJsonBinary, retroEntries)
	if err != nil {
		panic(err)
	}

	logger.Info("adding retroactive peer reward info")
	for _, s := range retroEntries.Rewards {
		peerId := s.PeerId
		peerBytes, err := base64.StdEncoding.DecodeString(peerId)
		if err != nil {
			panic(err)
		}

		addr, err := poseidon.HashBytes(peerBytes)
		if err != nil {
			panic(err)
		}

		addrBytes := addr.Bytes()
		addrBytes = append(make([]byte, 32-len(addrBytes)), addrBytes...)
		rewardTrie.Add(addrBytes, 0, s.TokenBalance)
	}

	trieBytes, err := rewardTrie.Serialize()
	if err != nil {
		panic(err)
	}

	ceremonyLobbyState := &protobufs.CeremonyLobbyState{
		LobbyState: 0,
		CeremonyState: &protobufs.CeremonyLobbyState_CeremonyOpenState{
			CeremonyOpenState: &protobufs.CeremonyOpenState{
				JoinedParticipants:    []*protobufs.CeremonyLobbyJoin{},
				PreferredParticipants: []*protobufs.Ed448PublicKey{},
			},
		},
		LatestTranscript: transcript,
		RewardTrie:       trieBytes,
	}
	outputBytes, err := proto.Marshal(ceremonyLobbyState)
	if err != nil {
		panic(err)
	}

	intrinsicFilter := append(
		p2p.GetBloomFilter(application.CEREMONY_ADDRESS, 256, 3),
		p2p.GetBloomFilterIndices(application.CEREMONY_ADDRESS, 65536, 24)...,
	)

	// Compat: there was a bug that went unnoticed in prior versions,
	// the raw filter was used instead of the application address, which didn't
	// affect execution because we forcibly stashed it. Preserving this to ensure
	// no rebuilding of frame history is required.
	executionOutput := &protobufs.IntrinsicExecutionOutput{
		Address: intrinsicFilter,
		Output:  outputBytes,
		Proof:   proofBytes,
	}

	data, err := proto.Marshal(executionOutput)
	if err != nil {
		panic(err)
	}

	logger.Info("proving execution output for inclusion")
	commitment, err := inclusionProver.Commit(
		data,
		protobufs.IntrinsicExecutionOutputType,
	)
	if err != nil {
		panic(err)
	}

	logger.Info("creating kzg proof")
	proof, err := inclusionProver.ProveAggregate(
		[]*qcrypto.InclusionCommitment{
			commitment,
		},
	)
	if err != nil {
		panic(err)
	}

	logger.Info("finalizing execution proof")

	return inputMessage, proof, proverKeys
}

// Start implements ExecutionEngine
func (e *CeremonyExecutionEngine) Start() <-chan error {
	errChan := make(chan error)

	e.logger.Info("ceremony data loaded", zap.Binary(
		"g2_power",
		kzg.CeremonyBLS48581G2[1].ToAffineCompressed(),
	))

	go func() {
		err := <-e.clock.Start()
		if err != nil {
			panic(err)
		}

		err = <-e.clock.RegisterExecutor(e, 0)
		if err != nil {
			panic(err)
		}

		go e.RunWorker()

		errChan <- nil
	}()

	return errChan
}

// Stop implements ExecutionEngine
func (e *CeremonyExecutionEngine) Stop(force bool) <-chan error {
	errChan := make(chan error)

	go func() {
		errChan <- <-e.clock.Stop(force)
	}()

	return errChan
}

// ProcessMessage implements ExecutionEngine
func (e *CeremonyExecutionEngine) ProcessMessage(
	address []byte,
	message *protobufs.Message,
) ([]*protobufs.Message, error) {
	if bytes.Equal(address, e.GetSupportedApplications()[0].Address) {
		e.logger.Debug("processing execution message")
		any := &anypb.Any{}
		if err := proto.Unmarshal(message.Payload, any); err != nil {
			return nil, errors.Wrap(err, "process message")
		}

		switch any.TypeUrl {
		case protobufs.ClockFrameType:
			frame := &protobufs.ClockFrame{}
			if err := any.UnmarshalTo(frame); err != nil {
				return nil, errors.Wrap(err, "process message")
			}

			if frame.FrameNumber < e.clock.GetFrame().FrameNumber {
				return nil, nil
			}

			if err := e.frameProver.VerifyDataClockFrame(frame); err != nil {
				return nil, errors.Wrap(err, "process message")
			}

			if err := e.VerifyExecution(frame); err != nil {
				return nil, errors.Wrap(err, "process message")
			}
		case protobufs.CeremonyLobbyJoinType:
			fallthrough
		case protobufs.CeremonySeenProverAttestationType:
			fallthrough
		case protobufs.CeremonyDroppedProverAttestationType:
			fallthrough
		case protobufs.CeremonyTranscriptCommitType:
			fallthrough
		case protobufs.CeremonyTranscriptShareType:
			fallthrough
		case protobufs.CeremonyTranscriptType:
			hash := sha3.Sum256(any.Value)
			if any.TypeUrl == protobufs.CeremonyTranscriptType {
				e.seenMessageMx.Lock()
				ref := string(hash[:])
				if _, ok := e.seenMessageMap[ref]; !ok {
					e.seenMessageMap[ref] = true
				} else {
					return nil, errors.Wrap(
						errors.New("message already received"),
						"process message",
					)
				}
				e.seenMessageMx.Unlock()
			}
			if e.clock.IsInProverTrie(e.proverPublicKey) {
				proposedTransition := &protobufs.CeremonyLobbyStateTransition{
					TypeUrls: []string{any.TypeUrl},
					TransitionInputs: [][]byte{
						any.Value,
					},
				}

				any := &anypb.Any{}
				if err := any.MarshalFrom(proposedTransition); err != nil {
					return nil, errors.Wrap(err, "process message")
				}

				any.TypeUrl = strings.Replace(
					any.TypeUrl,
					"type.googleapis.com",
					"types.quilibrium.com",
					1,
				)

				payload, err := proto.Marshal(any)
				if err != nil {
					return nil, errors.Wrap(err, "process message")
				}

				h, err := poseidon.HashBytes(payload)
				if err != nil {
					return nil, errors.Wrap(err, "process message")
				}

				msg := &protobufs.Message{
					Hash:    h.Bytes(),
					Address: e.provingKeyAddress,
					Payload: payload,
				}
				return []*protobufs.Message{
					msg,
				}, nil
			}
		}
	}

	return nil, nil
}

func (e *CeremonyExecutionEngine) RunWorker() {
	frameChan := e.clock.GetFrameChannel()
	for {
		select {
		case frame := <-frameChan:
			e.activeClockFrame = frame
			e.logger.Info(
				"evaluating next frame",
				zap.Uint64(
					"frame_number",
					frame.FrameNumber,
				),
			)
			app, err := application.MaterializeApplicationFromFrame(frame)
			if err != nil {
				e.logger.Error(
					"error while materializing application from frame",
					zap.Error(err),
				)
				panic(err)
			}

			_, _, reward := app.RewardTrie.Get(e.provingKeyAddress)
			_, _, retro := app.RewardTrie.Get(e.peerIdHash)
			e.logger.Info(
				"current application state",
				zap.Uint64("my_balance", reward+retro),
				zap.String("lobby_state", app.LobbyState.String()),
			)

			switch app.LobbyState {
			case application.CEREMONY_APPLICATION_STATE_OPEN:
				e.alreadyPublishedShare = false
				e.alreadyPublishedTranscript = false
				alreadyJoined := false
				for _, join := range app.LobbyJoins {
					if bytes.Equal(
						join.PublicKeySignatureEd448.PublicKey.KeyValue,
						e.proverPublicKey,
					) {
						alreadyJoined = true
						break
					}
				}

				e.logger.Info(
					"lobby open for joins",
					zap.Int("joined_participants", len(app.LobbyJoins)),
					zap.Int(
						"preferred_participants",
						len(app.NextRoundPreferredParticipants),
					),
					zap.Bool("in_lobby", alreadyJoined),
					zap.Uint64("state_count", app.StateCount),
				)

				if !alreadyJoined {
					e.logger.Info(
						"joining lobby",
						zap.Binary("proving_key", e.proverPublicKey),
					)
					if err := e.announceJoin(frame); err != nil {
						e.logger.Error(
							"failed to announce join",
							zap.Error(err),
						)
					}

					e.logger.Info("preparing contribution")
					// Calculate this now after announcing, this gives 10 frames of buffer
					e.ensureSecrets(app)
				}
			case application.CEREMONY_APPLICATION_STATE_IN_PROGRESS:
				inRound := false
				for _, p := range app.ActiveParticipants {
					if bytes.Equal(
						p.PublicKeySignatureEd448.PublicKey.KeyValue,
						e.proverPublicKey,
					) {
						inRound = true
						break
					}
				}

				if len(e.activeSecrets) == 0 && inRound {
					// If we ended up in the scenario where we do not have any secrets
					// available but we're in the round, we should politely leave.
					e.publishDroppedParticipant(e.proverPublicKey)
					continue
				}

				e.logger.Info(
					"round in progress",
					zap.Any("participants", app.ActiveParticipants),
					zap.Any(
						"current_seen_attestations",
						len(app.LatestSeenProverAttestations),
					),
					zap.Any(
						"current_dropped_attestations",
						len(app.DroppedParticipantAttestations),
					),
					zap.Any(
						"preferred_participants_for_next_round",
						len(app.NextRoundPreferredParticipants),
					),
					zap.Bool("in_round", inRound),
					zap.Uint64("current_sub_round", app.RoundCount),
					zap.Uint64("stale_state_count", app.StateCount),
				)

				shouldConnect := false
				position := 0
				if len(e.peerChannels) == 0 && app.RoundCount == 1 &&
					len(app.ActiveParticipants) > 1 {
					for i, p := range app.ActiveParticipants {
						if bytes.Equal(
							p.PublicKeySignatureEd448.PublicKey.KeyValue,
							e.proverPublicKey,
						) {
							shouldConnect = true
							position = i
							break
						}
					}
				}

				if shouldConnect {
					e.logger.Info(
						"connecting to peers",
						zap.Any("participants", app.ActiveParticipants),
					)
					err := e.connectToActivePeers(app, position)
					if err != nil {
						e.logger.Error("error while connecting to peers", zap.Error(err))
						e.publishDroppedParticipant(e.proverPublicKey)
						continue
					}
				}

				if len(e.peerChannels) != 0 {
					done := false
					rounds := app.TranscriptRoundAdvanceCommits
					if len(rounds) != 0 {
						for _, c := range rounds[app.RoundCount-1].Commits {
							if bytes.Equal(
								c.ProverSignature.PublicKey.KeyValue,
								e.proverPublicKey,
							) {
								done = true
							}
						}
					}

					if !done {
						e.logger.Info(
							"participating in round",
							zap.Any("participants", app.ActiveParticipants),
							zap.Uint64("current_round", app.RoundCount),
						)
						err := e.participateRound(app)
						if err != nil {
							e.logger.Error("error while participating in round", zap.Error(err))
							e.publishDroppedParticipant(e.proverPublicKey)
						}
					}
				} else if len(app.ActiveParticipants) == 1 &&
					bytes.Equal(
						app.ActiveParticipants[0].PublicKeySignatureEd448.PublicKey.KeyValue,
						e.proverPublicKey,
					) {
					if err = e.commitRound(e.activeSecrets); err != nil {
						e.logger.Error("error while participating in round", zap.Error(err))
					}
				}
			case application.CEREMONY_APPLICATION_STATE_FINALIZING:
				e.logger.Info(
					"round contribution finalizing",
					zap.Any("participants", len(app.ActiveParticipants)),
					zap.Any(
						"current_seen_attestations",
						len(app.LatestSeenProverAttestations),
					),
					zap.Any(
						"current_dropped_attestations",
						len(app.DroppedParticipantAttestations),
					),
					zap.Any(
						"preferred_participants_for_next_round",
						len(app.NextRoundPreferredParticipants),
					),
					zap.Int("finalized_shares", len(app.TranscriptShares)),
				)

				for _, s := range app.TranscriptShares {
					if bytes.Equal(
						s.ProverSignature.PublicKey.KeyValue,
						e.proverPublicKey,
					) {
						e.alreadyPublishedShare = true
					}
				}

				shouldPublish := false
				for _, p := range app.ActiveParticipants {
					if bytes.Equal(
						p.PublicKeySignatureEd448.PublicKey.KeyValue,
						e.proverPublicKey,
					) {
						shouldPublish = true
						break
					}
				}

				if !e.alreadyPublishedShare && shouldPublish {
					if len(e.activeSecrets) == 0 {
						e.publishDroppedParticipant(e.proverPublicKey)
						continue
					}
					err := e.publishTranscriptShare(app)
					if err != nil {
						e.logger.Error(
							"error while publishing transcript share",
							zap.Error(err),
						)
					}
				}
			case application.CEREMONY_APPLICATION_STATE_VALIDATING:
				e.logger.Info("round contribution validating")
				e.alreadyPublishedShare = false
				for _, c := range e.peerChannels {
					c.Close()
				}

				e.peerChannels = map[string]*p2p.PublicP2PChannel{}
				if app.UpdatedTranscript != nil && !e.alreadyPublishedTranscript {
					if err := e.publishTranscript(app); err != nil {
						e.logger.Error(
							"error while publishing transcript",
							zap.Error(err),
						)
					}
				}
			}
		}
	}
}

func (e *CeremonyExecutionEngine) publishMessage(
	filter []byte,
	message proto.Message,
) error {
	any := &anypb.Any{}
	if err := any.MarshalFrom(message); err != nil {
		return errors.Wrap(err, "publish message")
	}

	any.TypeUrl = strings.Replace(
		any.TypeUrl,
		"type.googleapis.com",
		"types.quilibrium.com",
		1,
	)

	payload, err := proto.Marshal(any)
	if err != nil {
		return errors.Wrap(err, "publish message")
	}

	h, err := poseidon.HashBytes(payload)
	if err != nil {
		return errors.Wrap(err, "publish message")
	}

	msg := &protobufs.Message{
		Hash:    h.Bytes(),
		Address: application.CEREMONY_ADDRESS,
		Payload: payload,
	}
	data, err := proto.Marshal(msg)
	if err != nil {
		return errors.Wrap(err, "publish message")
	}
	return e.pubSub.PublishToBitmask(filter, data)
}

func (e *CeremonyExecutionEngine) announceJoin(
	frame *protobufs.ClockFrame,
) error {
	idk, err := e.keyManager.GetAgreementKey("q-ratchet-idk")
	if err != nil {
		if errors.Is(err, keys.KeyNotFoundErr) {
			idk, err = e.keyManager.CreateAgreementKey(
				"q-ratchet-idk",
				keys.KeyTypeX448,
			)
			if err != nil {
				return errors.Wrap(err, "announce join")
			}
		} else {
			return errors.Wrap(err, "announce join")
		}
	}

	spk, err := e.keyManager.GetAgreementKey("q-ratchet-spk")
	if err != nil {
		if errors.Is(err, keys.KeyNotFoundErr) {
			spk, err = e.keyManager.CreateAgreementKey(
				"q-ratchet-spk",
				keys.KeyTypeX448,
			)
			if err != nil {
				return errors.Wrap(err, "announce join")
			}
		} else {
			return errors.Wrap(err, "announce join")
		}
	}

	g := curves.ED448().Point.Generator()

	join := &protobufs.CeremonyLobbyJoin{
		FrameNumber: frame.FrameNumber,
		IdentityKey: &protobufs.X448PublicKey{
			KeyValue: g.Mul(idk).ToAffineCompressed(),
		},
		SignedPreKey: &protobufs.X448PublicKey{
			KeyValue: g.Mul(spk).ToAffineCompressed(),
		},
		PeerId: e.pubSub.GetPeerID(),
	}
	sig, err := join.SignWithProverKey(e.provingKey)
	if err != nil {
		return errors.Wrap(err, "announce join")
	}

	join.PublicKeySignatureEd448 = &protobufs.Ed448Signature{
		Signature: sig,
		PublicKey: &protobufs.Ed448PublicKey{
			KeyValue: e.proverPublicKey,
		},
	}

	return errors.Wrap(
		e.publishMessage(
			e.intrinsicFilter,
			join,
		),
		"announce join",
	)
}

func (e *CeremonyExecutionEngine) connectToActivePeers(
	app *application.CeremonyApplication,
	position int,
) error {
	idk, err := e.keyManager.GetAgreementKey("q-ratchet-idk")
	if err != nil {
		return errors.Wrap(err, "connect to active peers")
	}
	spk, err := e.keyManager.GetAgreementKey("q-ratchet-spk")
	if err != nil {
		return errors.Wrap(err, "connect to active peers")
	}

	for i, p := range app.LobbyJoins {
		if !bytes.Equal(
			p.PublicKeySignatureEd448.PublicKey.KeyValue,
			e.proverPublicKey,
		) {
			receiverIdk, err := curves.ED448().Point.FromAffineCompressed(
				p.IdentityKey.KeyValue,
			)
			if err != nil {
				return errors.Wrap(err, "connect to active peers")
			}

			receiverSpk, err := curves.ED448().Point.FromAffineCompressed(
				p.SignedPreKey.KeyValue,
			)
			if err != nil {
				return errors.Wrap(err, "connect to active peers")
			}

			client, err := e.clock.GetPublicChannelForProvingKey(
				i > position,
				p.PeerId,
				p.PublicKeySignatureEd448.PublicKey.KeyValue,
			)
			if err != nil {
				e.logger.Error(
					"peer does not support direct public channels",
					zap.Binary(
						"proving_key",
						p.PublicKeySignatureEd448.PublicKey.KeyValue,
					),
					zap.Error(err),
				)
			}
			e.peerChannels[string(
				p.PublicKeySignatureEd448.PublicKey.KeyValue,
			)], err = p2p.NewPublicP2PChannel(
				client,
				e.proverPublicKey,
				p.PublicKeySignatureEd448.PublicKey.KeyValue,
				i > position,
				idk,
				spk,
				receiverIdk,
				receiverSpk,
				curves.ED448(),
				e.keyManager,
				e.pubSub,
			)
			if err != nil {
				e.logger.Error(
					"could not establish p2p channel",
					zap.Binary(
						"proving_key",
						p.PublicKeySignatureEd448.PublicKey.KeyValue,
					),
					zap.Error(err),
				)
				return errors.Wrap(err, "connect to active peers")
			}
		}
	}

	return nil
}

func (e *CeremonyExecutionEngine) participateRound(
	app *application.CeremonyApplication,
) error {
	idk, err := e.keyManager.GetAgreementKey("q-ratchet-idk")
	if err != nil {
		return errors.Wrap(err, "participate round")
	}

	spk, err := e.keyManager.GetAgreementKey("q-ratchet-spk")
	if err != nil {
		return errors.Wrap(err, "participate round")
	}

	idkPoint := curves.ED448().Point.Generator().Mul(idk)
	idks := []curves.Point{}
	initiator := false
	for _, p := range app.ActiveParticipants {
		if !bytes.Equal(
			p.PublicKeySignatureEd448.PublicKey.KeyValue,
			e.proverPublicKey,
		) {
			ic, err := e.keyStore.GetLatestKeyBundle(
				p.PublicKeySignatureEd448.PublicKey.KeyValue,
			)
			if err != nil {
				return errors.Wrap(err, "participate round")
			}

			var kba *protobufs.KeyBundleAnnouncement
			switch ic.TypeUrl {
			case protobufs.KeyBundleAnnouncementType:
				kba = &protobufs.KeyBundleAnnouncement{}
				if err := proto.Unmarshal(
					ic.Data,
					kba,
				); err != nil {
					return errors.Wrap(err, "participate round")
				}
			}

			receiverIdk, err := curves.ED448().Point.FromAffineCompressed(
				kba.IdentityKey.GetPublicKeySignatureEd448().PublicKey.KeyValue,
			)
			if err != nil {
				return errors.Wrap(err, "participate round")
			}

			receiverSpk, err := curves.ED448().Point.FromAffineCompressed(
				kba.SignedPreKey.GetPublicKeySignatureEd448().PublicKey.KeyValue,
			)
			if err != nil {
				return errors.Wrap(err, "participate round")
			}

			if _, ok := e.peerChannels[string(
				p.PublicKeySignatureEd448.PublicKey.KeyValue,
			)]; !ok {
				client, err := e.clock.GetPublicChannelForProvingKey(
					initiator,
					p.PeerId,
					p.PublicKeySignatureEd448.PublicKey.KeyValue,
				)
				if err != nil {
					e.logger.Error(
						"peer does not support direct public channels",
						zap.Binary(
							"proving_key",
							p.PublicKeySignatureEd448.PublicKey.KeyValue,
						),
						zap.Error(err),
					)
				}
				e.peerChannels[string(
					p.PublicKeySignatureEd448.PublicKey.KeyValue,
				)], err = p2p.NewPublicP2PChannel(
					client,
					e.proverPublicKey,
					p.PublicKeySignatureEd448.PublicKey.KeyValue,
					initiator,
					idk,
					spk,
					receiverIdk,
					receiverSpk,
					curves.ED448(),
					e.keyManager,
					e.pubSub,
				)
				if err != nil {
					return errors.Wrap(err, "participate round")
				}
			}

			idks = append(idks, receiverIdk)
		} else {
			initiator = true
			idks = append(idks, idkPoint)
		}
	}

	pubKeys := [][]byte{}
	for _, p := range app.ActiveParticipants {
		pubKeys = append(
			pubKeys,
			p.PublicKeySignatureEd448.PublicKey.KeyValue,
		)
	}

	newSecrets, err := application.ProcessRound(
		e.proverPublicKey,
		idk,
		int(app.RoundCount),
		pubKeys,
		idks,
		e.activeSecrets,
		curves.BLS48581G1(),
		func(i int, receiver []byte, msg []byte) error {
			return e.peerChannels[string(receiver)].Send(msg)
		},
		func(i int, sender []byte) ([]byte, error) {
			msg, err := e.peerChannels[string(
				sender,
			)].Receive()
			if err != nil {
				e.publishDroppedParticipant(sender)
				return nil, err
			} else {
				if i == 0 {
					e.publishLastSeenParticipant(sender)
				}
				return msg, nil
			}
		},
		app.LatestTranscript.G1Powers[1].KeyValue,
	)
	if err != nil {
		return errors.Wrap(err, "participate round")
	}

	return errors.Wrap(e.commitRound(newSecrets), "participate round")
}

func (e *CeremonyExecutionEngine) commitRound(secrets []curves.Scalar) error {
	g2Pub := curves.BLS48581G2().Point.Generator().Mul(secrets[0])

	sig, err := application.SignProverKeyForCommit(
		e.proverPublicKey,
		secrets[0],
	)
	if err != nil {
		return errors.Wrap(err, "commit round")
	}

	proverSig, err := e.provingKey.Sign(
		rand.Reader,
		g2Pub.ToAffineCompressed(),
		crypto.Hash(0),
	)
	if err != nil {
		return errors.Wrap(err, "commit round")
	}

	advance := &protobufs.CeremonyTranscriptCommit{
		ProverSignature: &protobufs.Ed448Signature{
			Signature: proverSig,
			PublicKey: &protobufs.Ed448PublicKey{
				KeyValue: e.proverPublicKey,
			},
		},
		ContributionSignature: &protobufs.BLS48581Signature{
			Signature: sig,
			PublicKey: &protobufs.BLS48581G2PublicKey{
				KeyValue: g2Pub.ToAffineCompressed(),
			},
		},
	}

	if err := e.publishMessage(
		e.intrinsicFilter,
		advance,
	); err != nil {
		return errors.Wrap(err, "commit round")
	}

	e.activeSecrets = secrets
	return nil
}

// Publishes a dropped participant attestation, logs any errors but does not
// forward them on.
func (e *CeremonyExecutionEngine) publishDroppedParticipant(
	participant []byte,
) {
	frameNumber := e.clock.GetFrame().FrameNumber

	b := binary.BigEndian.AppendUint64([]byte("dropped"), frameNumber)
	b = append(b, participant...)
	sig, err := e.provingKey.Sign(rand.Reader, b, crypto.Hash(0))
	if err != nil {
		e.logger.Error(
			"error while signing dropped participant attestation",
			zap.Error(err),
		)
		return
	}

	dropped := &protobufs.CeremonyDroppedProverAttestation{
		DroppedProverKey: &protobufs.Ed448PublicKey{
			KeyValue: participant,
		},
		LastSeenFrame: frameNumber,
		ProverSignature: &protobufs.Ed448Signature{
			Signature: sig,
			PublicKey: &protobufs.Ed448PublicKey{
				KeyValue: e.proverPublicKey,
			},
		},
	}

	err = e.publishMessage(
		e.intrinsicFilter,
		dropped,
	)
	if err != nil {
		e.logger.Error(
			"error while publishing dropped participant attestation",
			zap.Error(err),
		)
		return
	}
}

// Publishes a last seen participant attestation, logs any errors but does not
// forward them on.
func (e *CeremonyExecutionEngine) publishLastSeenParticipant(
	participant []byte,
) {
	frameNumber := e.clock.GetFrame().FrameNumber

	b := binary.BigEndian.AppendUint64([]byte("lastseen"), frameNumber)
	b = append(b, participant...)
	sig, err := e.provingKey.Sign(rand.Reader, b, crypto.Hash(0))
	if err != nil {
		e.logger.Error(
			"error while signing last seen participant attestation",
			zap.Error(err),
		)
		return
	}

	seen := &protobufs.CeremonySeenProverAttestation{
		SeenProverKey: &protobufs.Ed448PublicKey{
			KeyValue: participant,
		},
		LastSeenFrame: frameNumber,
		ProverSignature: &protobufs.Ed448Signature{
			Signature: sig,
			PublicKey: &protobufs.Ed448PublicKey{
				KeyValue: e.proverPublicKey,
			},
		},
	}
	err = e.publishMessage(
		e.intrinsicFilter,
		seen,
	)
	if err != nil {
		e.logger.Error(
			"error while publishing dropped participant attestation",
			zap.Error(err),
		)
		return
	}
}

func (e *CeremonyExecutionEngine) publishTranscriptShare(
	app *application.CeremonyApplication,
) error {
	transcriptShare := &protobufs.CeremonyTranscriptShare{}
	transcriptShare.AdditiveG1Powers = make(
		[]*protobufs.BLS48581G1PublicKey,
		len(e.activeSecrets),
	)
	transcriptShare.AdditiveG2Powers = make(
		[]*protobufs.BLS48581G2PublicKey,
		len(app.LatestTranscript.G2Powers)-1,
	)

	eg := errgroup.Group{}
	eg.SetLimit(100)
	e.logger.Info("creating transcript share")
	for i, s := range e.activeSecrets {
		i := i
		s := s
		eg.Go(func() error {
			if i%100 == 0 {
				e.logger.Info(
					"writing transcript share chunk",
					zap.Int("chunk_start", i),
				)
			}

			basisG1, err := curves.BLS48581G1().Point.FromAffineCompressed(
				app.LatestTranscript.G1Powers[i+1].KeyValue,
			)
			if err != nil {
				return errors.Wrap(err, "publish transcript share")
			}

			transcriptShare.AdditiveG1Powers[i] = &protobufs.BLS48581G1PublicKey{
				KeyValue: basisG1.Mul(s).ToAffineCompressed(),
			}

			if i+1 < len(app.LatestTranscript.G2Powers) {
				basisG2, err := curves.BLS48581G2().Point.FromAffineCompressed(
					app.LatestTranscript.G2Powers[i+1].KeyValue,
				)
				if err != nil {
					return errors.Wrap(err, "publish transcript share")
				}

				transcriptShare.AdditiveG2Powers[i] = &protobufs.BLS48581G2PublicKey{
					KeyValue: basisG2.Mul(s).ToAffineCompressed(),
				}
			}
			return nil
		})
	}

	if err := eg.Wait(); err != nil {
		return err
	}

	e.logger.Info(
		"done writing transcript chunks, adding witnesses and signing",
	)

	transcriptShare.AdditiveG1_256Witness = &protobufs.BLS48581G1PublicKey{
		KeyValue: curves.BLS48581G1().Point.Generator().Mul(
			e.activeSecrets[len(app.LatestTranscript.G2Powers)-2],
		).ToAffineCompressed(),
	}

	transcriptShare.AdditiveG2_256Witness = &protobufs.BLS48581G2PublicKey{
		KeyValue: curves.BLS48581G2().Point.Generator().Mul(
			e.activeSecrets[len(app.LatestTranscript.G2Powers)-2],
		).ToAffineCompressed(),
	}

	sig, err := transcriptShare.SignWithProverKey(e.provingKey)
	if err != nil {
		errors.Wrap(err, "publish transcript share")
	}

	transcriptShare.ProverSignature = &protobufs.Ed448Signature{
		Signature: sig,
		PublicKey: &protobufs.Ed448PublicKey{
			KeyValue: e.proverPublicKey,
		},
	}

	err = errors.Wrap(
		e.publishMessage(
			e.intrinsicFilter,
			transcriptShare,
		),
		"publish transcript share",
	)
	if err != nil {
		return err
	} else {
		e.alreadyPublishedShare = true
		return nil
	}
}

func (e *CeremonyExecutionEngine) VerifyExecution(
	frame *protobufs.ClockFrame,
) error {
	if e.clock.GetFrame().FrameNumber != frame.FrameNumber-1 {
		return nil
	}

	if len(frame.AggregateProofs) > 0 {
		for _, proofs := range frame.AggregateProofs {
			for _, inclusion := range proofs.InclusionCommitments {
				if inclusion.TypeUrl == protobufs.IntrinsicExecutionOutputType {
					transition, _, err := application.GetOutputsFromClockFrame(frame)
					if err != nil {
						return errors.Wrap(err, "verify execution")
					}

					parent, err := e.clockStore.GetStagedDataClockFrame(
						append(
							p2p.GetBloomFilter(application.CEREMONY_ADDRESS, 256, 3),
							p2p.GetBloomFilterIndices(
								application.CEREMONY_ADDRESS,
								65536,
								24,
							)...,
						),
						frame.FrameNumber-1,
						frame.ParentSelector,
						false,
					)
					if err != nil && !errors.Is(err, store.ErrNotFound) {
						return errors.Wrap(err, "verify execution")
					}

					if parent == nil {
						return errors.Wrap(
							errors.New("missing parent frame"),
							"verify execution",
						)
					}

					a, err := application.MaterializeApplicationFromFrame(parent)
					if err != nil {
						return errors.Wrap(err, "verify execution")
					}

					a, _, _, err = a.ApplyTransition(frame.FrameNumber, transition, false)
					if err != nil {
						return errors.Wrap(err, "verify execution")
					}

					a2, err := application.MaterializeApplicationFromFrame(frame)
					if err != nil {
						return errors.Wrap(err, "verify execution")
					}

					if !a.Equals(a2) {
						return errors.Wrap(
							application.ErrInvalidStateTransition,
							"verify execution",
						)
					}

					return nil
				}
			}
		}
	}

	return nil
}

func (e *CeremonyExecutionEngine) publishTranscript(
	app *application.CeremonyApplication,
) error {
	e.logger.Info("publishing updated transcript")
	e.alreadyPublishedTranscript = true
	err := errors.Wrap(
		e.publishMessage(
			e.intrinsicFilter,
			app.UpdatedTranscript,
		),
		"publish transcript share",
	)
	if err != nil {
		e.alreadyPublishedTranscript = false
		return err
	} else {
		return nil
	}
}

func (e *CeremonyExecutionEngine) ensureSecrets(
	app *application.CeremonyApplication,
) {
	if len(e.activeSecrets) == 0 {
		e.activeSecrets = []curves.Scalar{}
		t := curves.BLS48581G1().Scalar.Random(rand.Reader)
		x := t.Clone()

		for i := 0; i < len(app.LatestTranscript.G1Powers)-1; i++ {
			if i%1000 == 0 {
				e.logger.Info(
					"calculating secrets for contribution",
					zap.Int("secrets_calculated", i),
					zap.Int("total_secrets", len(app.LatestTranscript.G1Powers)-1),
				)
			}
			e.activeSecrets = append(e.activeSecrets, x)
			x = x.Mul(t)
		}

		e.logger.Info(
			"done preparing contribution",
			zap.Int("secrets_calculated", len(e.activeSecrets)),
		)
	}
}

func (e *CeremonyExecutionEngine) GetPeerInfo() *protobufs.PeerInfoResponse {
	return e.clock.GetPeerInfo()
}

func (e *CeremonyExecutionEngine) GetFrame() *protobufs.ClockFrame {
	return e.clock.GetFrame()
}
