Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions core/parsigex/parsigex.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func Protocols() []protocol.ID {
}

func NewParSigEx(p2pNode host.Host, sendFunc p2p.SendFunc, peerIdx int, peers []peer.ID,
verifyFunc func(context.Context, core.Duty, core.PubKey, core.ParSignedData) error,
verifyFunc func(context.Context, peer.ID, core.Duty, core.PubKey, core.ParSignedData) error,
gaterFunc core.DutyGaterFunc, p2pOpts ...p2p.SendRecvOption,
) *ParSigEx {
parSigEx := &ParSigEx{
Expand Down Expand Up @@ -72,12 +72,12 @@ type ParSigEx struct {
sendFunc p2p.SendFunc
peerIdx int
peers []peer.ID
verifyFunc func(context.Context, core.Duty, core.PubKey, core.ParSignedData) error
verifyFunc func(context.Context, peer.ID, core.Duty, core.PubKey, core.ParSignedData) error
gaterFunc core.DutyGaterFunc
subs []func(context.Context, core.Duty, core.ParSignedDataSet) error
}

func (m *ParSigEx) handle(ctx context.Context, _ peer.ID, req proto.Message) (proto.Message, bool, error) {
func (m *ParSigEx) handle(ctx context.Context, sender peer.ID, req proto.Message) (proto.Message, bool, error) {
pb, ok := req.(*pbv1.ParSigExMsg)
if !ok {
return nil, false, errors.New("invalid request type")
Expand Down Expand Up @@ -110,7 +110,7 @@ func (m *ParSigEx) handle(ctx context.Context, _ peer.ID, req proto.Message) (pr
verifyStart := time.Now()

for pubkey, data := range set {
if err = m.verifyFunc(ctx, duty, pubkey, data); err != nil {
if err = m.verifyFunc(ctx, sender, duty, pubkey, data); err != nil {
return nil, false, errors.Wrap(err, "invalid partial signature")
}
}
Expand Down Expand Up @@ -165,8 +165,10 @@ func (m *ParSigEx) Subscribe(fn func(context.Context, core.Duty, core.ParSignedD
}

// NewEth2Verifier returns a partial signature verification function for core workflow eth2 signatures.
func NewEth2Verifier(eth2Cl eth2wrap.Client, pubSharesByKey map[core.PubKey]map[int]tbls.PublicKey) (func(context.Context, core.Duty, core.PubKey, core.ParSignedData) error, error) {
return func(ctx context.Context, duty core.Duty, pubkey core.PubKey, data core.ParSignedData) error {
// Core workflow partial signatures are verified cryptographically against the pubshare for the
// claimed share index, so the authenticated sender is not needed here.
func NewEth2Verifier(eth2Cl eth2wrap.Client, pubSharesByKey map[core.PubKey]map[int]tbls.PublicKey) (func(context.Context, peer.ID, core.Duty, core.PubKey, core.ParSignedData) error, error) {
return func(ctx context.Context, _ peer.ID, duty core.Duty, pubkey core.PubKey, data core.ParSignedData) error {
pubshares, ok := pubSharesByKey[pubkey]
if !ok {
return errors.New("unknown pubkey, not part of cluster lock")
Expand Down
26 changes: 13 additions & 13 deletions core/parsigex/parsigex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestParSigEx(t *testing.T) {
}
}

verifyFunc := func(context.Context, core.Duty, core.PubKey, core.ParSignedData) error {
verifyFunc := func(context.Context, peer.ID, core.Duty, core.PubKey, core.ParSignedData) error {
return nil
}

Expand Down Expand Up @@ -171,7 +171,7 @@ func TestParSigExVerifier(t *testing.T) {
att.Deneb.Signature = sign(sigData[:])
data, err := core.NewPartialVersionedAttestation(att, shareIdx)
require.NoError(t, err)
require.NoError(t, verifyFunc(ctx, core.NewAttesterDuty(slot), pubkey, data))
require.NoError(t, verifyFunc(ctx, "", core.NewAttesterDuty(slot), pubkey, data))
})

t.Run("Verify proposal", func(t *testing.T) {
Expand All @@ -186,7 +186,7 @@ func TestParSigExVerifier(t *testing.T) {
data, err := core.NewPartialVersionedSignedProposal(proposal, shareIdx)
require.NoError(t, err)

require.NoError(t, verifyFunc(ctx, core.NewProposerDuty(slot), pubkey, data))
require.NoError(t, verifyFunc(ctx, "", core.NewProposerDuty(slot), pubkey, data))
})

t.Run("Verify blinded proposal", func(t *testing.T) {
Expand All @@ -204,7 +204,7 @@ func TestParSigExVerifier(t *testing.T) {
data, err := core.NewPartialVersionedSignedBlindedProposal(&eth2apiBlinded, shareIdx)
require.NoError(t, err)

require.NoError(t, verifyFunc(ctx, core.NewProposerDuty(slot), pubkey, data))
require.NoError(t, verifyFunc(ctx, "", core.NewProposerDuty(slot), pubkey, data))
})

t.Run("Verify Randao", func(t *testing.T) {
Expand All @@ -216,7 +216,7 @@ func TestParSigExVerifier(t *testing.T) {

randao := core.NewPartialSignedRandao(epoch, sign(sigData[:]), shareIdx)

require.NoError(t, verifyFunc(ctx, core.NewRandaoDuty(slot), pubkey, randao))
require.NoError(t, verifyFunc(ctx, "", core.NewRandaoDuty(slot), pubkey, randao))
})

t.Run("Verify Voluntary Exit", func(t *testing.T) {
Expand All @@ -232,7 +232,7 @@ func TestParSigExVerifier(t *testing.T) {

require.NoError(t, err)

require.NoError(t, verifyFunc(ctx, core.NewVoluntaryExit(slot), pubkey, data))
require.NoError(t, verifyFunc(ctx, "", core.NewVoluntaryExit(slot), pubkey, data))
})

t.Run("Verify validator registration", func(t *testing.T) {
Expand All @@ -249,7 +249,7 @@ func TestParSigExVerifier(t *testing.T) {
data, err := core.NewPartialVersionedSignedValidatorRegistration(&reg.VersionedSignedValidatorRegistration, shareIdx)
require.NoError(t, err)

require.NoError(t, verifyFunc(ctx, core.NewBuilderRegistrationDuty(slot), pubkey, data))
require.NoError(t, verifyFunc(ctx, "", core.NewBuilderRegistrationDuty(slot), pubkey, data))
})

t.Run("Verify beacon committee selection", func(t *testing.T) {
Expand All @@ -263,7 +263,7 @@ func TestParSigExVerifier(t *testing.T) {
selection.SelectionProof = sign(sigData[:])
data := core.NewPartialSignedBeaconCommitteeSelection(selection, shareIdx)

require.NoError(t, verifyFunc(ctx, core.NewPrepareAggregatorDuty(slot), pubkey, data))
require.NoError(t, verifyFunc(ctx, "", core.NewPrepareAggregatorDuty(slot), pubkey, data))
})

t.Run("Verify aggregate and proof", func(t *testing.T) {
Expand All @@ -286,7 +286,7 @@ func TestParSigExVerifier(t *testing.T) {
agg.Deneb.Signature = sign(sigData[:])
data := core.NewPartialVersionedSignedAggregateAndProof(agg, shareIdx)

require.NoError(t, verifyFunc(ctx, core.NewAggregatorDuty(slot), pubkey, data))
require.NoError(t, verifyFunc(ctx, "", core.NewAggregatorDuty(slot), pubkey, data))
})

t.Run("verify sync committee message", func(t *testing.T) {
Expand All @@ -299,11 +299,11 @@ func TestParSigExVerifier(t *testing.T) {
msg.Signature = sign(sigData[:])

data := core.NewPartialSignedSyncMessage(msg, shareIdx)
require.NoError(t, verifyFunc(ctx, core.NewSyncMessageDuty(slot), pubkey, data))
require.NoError(t, verifyFunc(ctx, "", core.NewSyncMessageDuty(slot), pubkey, data))

// Invalid sync committee message.
data = core.NewPartialSignedRandao(epoch, testutil.RandomEth2Signature(), shareIdx)
err = verifyFunc(ctx, core.NewSyncMessageDuty(slot), pubkey, data)
err = verifyFunc(ctx, "", core.NewSyncMessageDuty(slot), pubkey, data)
require.Error(t, err)
require.ErrorContains(t, err, "invalid signature")
})
Expand All @@ -326,7 +326,7 @@ func TestParSigExVerifier(t *testing.T) {

parSigData := core.NewPartialSignedSyncCommitteeSelection(selection, shareIdx)

require.NoError(t, verifyFunc(ctx, core.NewPrepareSyncContributionDuty(slot), pubkey, parSigData))
require.NoError(t, verifyFunc(ctx, "", core.NewPrepareSyncContributionDuty(slot), pubkey, parSigData))
})

t.Run("verify sync committee contribution and proof", func(t *testing.T) {
Expand All @@ -343,7 +343,7 @@ func TestParSigExVerifier(t *testing.T) {

parSigData := core.NewPartialSignedSyncContributionAndProof(proof, shareIdx)

require.NoError(t, verifyFunc(ctx, core.NewPrepareSyncContributionDuty(slot), pubkey, parSigData))
require.NoError(t, verifyFunc(ctx, "", core.NewPrepareSyncContributionDuty(slot), pubkey, parSigData))
})
}

Expand Down
21 changes: 12 additions & 9 deletions dkg/dkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,24 +241,27 @@ func Run(ctx context.Context, conf Config) (err error) {
return errors.Wrap(err, "get peer IDs")
}

ex := newExchanger(p2pNode, nodeIdx.PeerIdx, peerIDs, []sigType{
sigLock,
sigDepositData,
sigValidatorRegistration,
}, conf.Timeout)

// Register libp2p handlers
peerMap := make(map[peer.ID]cluster.NodeIdx)

for _, p := range peers {
nodeIdx, err := def.NodeIdx(p.ID)
idx, err := def.NodeIdx(p.ID)
if err != nil {
return err
}

peerMap[p.ID] = nodeIdx
peerMap[p.ID] = idx
}

ex, err := newExchanger(p2pNode, nodeIdx.PeerIdx, peerIDs, peerMap, []sigType{
sigLock,
sigDepositData,
sigValidatorRegistration,
}, conf.Timeout)
if err != nil {
return err
}

// Register libp2p handlers
caster := bcast.New(p2pNode, peerIDs, key)

// register bcast callbacks for frostp2p
Expand Down
46 changes: 40 additions & 6 deletions dkg/exchanger.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/z"
"github.com/obolnetwork/charon/cluster"
"github.com/obolnetwork/charon/core"
"github.com/obolnetwork/charon/core/parsigdb"
"github.com/obolnetwork/charon/core/parsigex"
Expand Down Expand Up @@ -67,10 +68,25 @@ type exchanger struct {
dutyGaterFunc func(duty core.Duty) bool
}

func newExchanger(p2pNode host.Host, peerIdx int, peers []peer.ID, sigTypes []sigType, timeout time.Duration) *exchanger {
// Partial signature roots not known yet, so skip verification in parsigex, rather verify before we aggregate.
noopVerifier := func(context.Context, core.Duty, core.PubKey, core.ParSignedData) error {
return nil
func newExchanger(p2pNode host.Host, peerIdx int, peers []peer.ID, peerMap map[peer.ID]cluster.NodeIdx, sigTypes []sigType, timeout time.Duration) (*exchanger, error) {
if peerIdx < 0 || peerIdx >= len(peers) {
return nil, errors.New("peer index out of range", z.Int("peer_idx", peerIdx), z.Int("num_peers", len(peers)))
}

// Every peer must have a valid share index in peerMap, otherwise its partial signatures would be
// rejected as unknown and the exchange would silently time out. Fail fast on a misconfigured map.
for _, p := range peers {
if nodeIdx, ok := peerMap[p]; !ok || nodeIdx.ShareIdx <= 0 {
return nil, errors.New("peer map missing valid share index for peer", z.Str("peer", p.String()))
}
}

// Partial signature roots are not known during the exchange, so cryptographic verification is
// deferred until aggregation. Each received partial signature is still bound to its authenticated
// sender: a peer may only contribute partial signatures under its own assigned share index,
// consistent with the sender check in nodesigs.go.
verifyShareIdx := func(_ context.Context, sender peer.ID, _ core.Duty, _ core.PubKey, data core.ParSignedData) error {
return verifyPeerShareIdx(peerMap, sender, data)
}

st := make(map[sigType]bool)
Expand All @@ -94,7 +110,7 @@ func newExchanger(p2pNode host.Host, peerIdx int, peers []peer.ID, sigTypes []si
ex := &exchanger{
// threshold is len(peers) to wait until we get all the partial sigs from all the peers per DV
sigdb: parsigdb.NewMemDB(len(peers), noopDeadliner{}, parsigdb.NewMemDBMetadata(0, time.Now())), // metadata timestamps are used for metrics, irrelevant for DKG
sigex: parsigex.NewParSigEx(p2pNode, p2p.Send, peerIdx, peers, noopVerifier, dutyGaterFunc, p2p.WithSendTimeout(timeout), p2p.WithReceiveTimeout(timeout)),
sigex: parsigex.NewParSigEx(p2pNode, p2p.Send, peerIdx, peers, verifyShareIdx, dutyGaterFunc, p2p.WithSendTimeout(timeout), p2p.WithReceiveTimeout(timeout)),
sigTypes: st,
sigData: dataByPubkey{
store: sigTypeStore{},
Expand All @@ -108,7 +124,25 @@ func newExchanger(p2pNode host.Host, peerIdx int, peers []peer.ID, sigTypes []si
ex.sigdb.SubscribeThreshold(ex.pushPsigs)
ex.sigex.Subscribe(ex.sigdb.StoreExternal)

return ex
return ex, nil
}

// verifyPeerShareIdx checks that a received partial signature originates from a known peer and uses
// that peer's assigned share index. peerMap maps each participating peer to its node index, so the
// check remains correct when share indices are not contiguous with peer positions (for example when
// operators have been removed and the remaining ones keep their original share indices).
func verifyPeerShareIdx(peerMap map[peer.ID]cluster.NodeIdx, sender peer.ID, data core.ParSignedData) error {
nodeIdx, ok := peerMap[sender]
if !ok {
return errors.New("partial signature from unknown peer", z.Str("peer", sender.String()))
}

if data.ShareIdx <= 0 || data.ShareIdx != nodeIdx.ShareIdx {
return errors.New("partial signature share index does not match sender peer",
z.Str("peer", sender.String()), z.Int("share_idx", data.ShareIdx), z.Int("expected_share_idx", nodeIdx.ShareIdx))
}

return nil
}

// exchange exchanges partial signatures of lockhash/deposit-data among dkg participants and returns all the partial
Expand Down
Loading
Loading