From 4260ea454fb1695fea0252d6d972747f15b1e7a1 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Tue, 19 May 2026 10:20:03 +0200 Subject: [PATCH] Cherry-pick #4526 PR --- app/obolapi/deposit.go | 21 +++++++++++++++-- app/obolapi/deposit_model.go | 3 +-- app/obolapi/deposit_test.go | 24 ++++++++++++------- cmd/depositfetch.go | 15 +++++++++++- eth2util/deposit/deposit.go | 9 ++++++- testutil/obolapimock/deposit.go | 42 +++++++++++++-------------------- 6 files changed, 74 insertions(+), 40 deletions(-) diff --git a/app/obolapi/deposit.go b/app/obolapi/deposit.go index 8386856906..66a4a811cc 100644 --- a/app/obolapi/deposit.go +++ b/app/obolapi/deposit.go @@ -77,7 +77,7 @@ func (c Client) PostPartialDeposits(ctx context.Context, lockHash []byte, shareI // GetFullDeposit gets the full deposit message for a given validator public key, lock hash and share index. // It respects the timeout specified in the Client instance. -func (c Client) GetFullDeposit(ctx context.Context, valPubkey string, lockHash []byte, threshold int) ([]eth2p0.DepositData, error) { +func (c Client) GetFullDeposit(ctx context.Context, valPubkey string, lockHash []byte, threshold int, partialPubKeys [][]byte) ([]eth2p0.DepositData, error) { valPubkeyBytes, err := from0x(valPubkey, len(eth2p0.BLSPubKey{})) if err != nil { return []eth2p0.DepositData{}, errors.Wrap(err, "validator pubkey to bytes") @@ -112,6 +112,12 @@ func (c Client) GetFullDeposit(ctx context.Context, valPubkey string, lockHash [ return []eth2p0.DepositData{}, errors.Wrap(err, "withdrawal credentials to bytes") } + // Build a hex-encoded public-share -> 1-based share-index lookup so we can resolve the correct index. + partialPubKeyToIdx := make(map[string]int, len(partialPubKeys)) + for i, parPubKey := range partialPubKeys { + partialPubKeyToIdx[hex.EncodeToString(parPubKey)] = i + 1 + } + // do aggregation fullDeposits := []eth2p0.DepositData{} @@ -147,7 +153,18 @@ func (c Client) GetFullDeposit(ctx context.Context, valPubkey string, lockHash [ return []eth2p0.DepositData{}, errors.Wrap(err, "invalid partial signature") } - rawSignatures[sigIdx+1] = sig + shareIdx := sigIdx + 1 + + if pk := strings.TrimPrefix(strings.ToLower(sigStr.PartialPublicKey), "0x"); pk != "" { + idx, ok := partialPubKeyToIdx[pk] + if !ok { + return []eth2p0.DepositData{}, errors.New("partial public key not found in validator public shares", z.Str("partial_public_key", sigStr.PartialPublicKey)) + } + + shareIdx = idx + } + + rawSignatures[shareIdx] = sig } fullSig, err := tbls.ThresholdAggregate(rawSignatures) diff --git a/app/obolapi/deposit_model.go b/app/obolapi/deposit_model.go index 22b33a80a9..1925fed179 100644 --- a/app/obolapi/deposit_model.go +++ b/app/obolapi/deposit_model.go @@ -12,9 +12,8 @@ type PartialDepositRequest struct { // FullDepositResponse contains all partial signatures, public key, amounts and withdrawal credentials to construct // a full deposit message for a validator. -// Signatures are ordered by share index. type FullDepositResponse struct { - PublicKey string `json:"public_key"` + PublicKey string `json:"pubkey"` WithdrawalCredentials string `json:"withdrawal_credentials"` Amounts []Amount `json:"amounts"` } diff --git a/app/obolapi/deposit_test.go b/app/obolapi/deposit_test.go index 4d83f695fe..5293621966 100644 --- a/app/obolapi/deposit_test.go +++ b/app/obolapi/deposit_test.go @@ -22,7 +22,12 @@ import ( ) func TestAPIDeposit(t *testing.T) { - kn := 4 + // Use a 3-of-5 cluster and submit from non-contiguous nodes (2, 4, 5) to + // exercise correct share-index mapping in ThresholdAggregate. + const ( + numNodes = 5 + threshold = 3 + ) beaconMock, err := beaconmock.New(t.Context()) require.NoError(t, err) @@ -40,11 +45,11 @@ func TestAPIDeposit(t *testing.T) { random := rand.New(rand.NewSource(int64(0))) - lock, peers, shares := cluster.NewForT( + lock, _, shares := cluster.NewForT( t, 1, - kn, - kn, + threshold, + numNodes, 0, random, ) @@ -69,7 +74,10 @@ func TestAPIDeposit(t *testing.T) { cl, err := obolapi.New(srv.URL) require.NoError(t, err) - for idx := range len(peers) { + // Submit from nodes 2, 4, 5 (0-indexed: 1, 3, 4) — non-contiguous share indices. + for _, idx := range []int{1, 3, 4} { + shareIndex := uint64(idx + 1) + signature, err := tbls.Sign(shares[0][idx], depositMessageSigRoot[:]) require.NoError(t, err) @@ -80,11 +88,9 @@ func TestAPIDeposit(t *testing.T) { Signature: eth2p0.BLSSignature(signature), } - // send all the partial deposits - require.NoError(t, cl.PostPartialDeposits(t.Context(), lock.LockHash, uint64(idx+1), []eth2p0.DepositData{depositData}), "share index: %d", idx+1) + require.NoError(t, cl.PostPartialDeposits(t.Context(), lock.LockHash, shareIndex, []eth2p0.DepositData{depositData}), "share index: %d", shareIndex) } - // get full exit - _, err = cl.GetFullDeposit(t.Context(), lock.Validators[0].PublicKeyHex(), lock.LockHash, lock.Threshold) + _, err = cl.GetFullDeposit(t.Context(), lock.Validators[0].PublicKeyHex(), lock.LockHash, lock.Threshold, lock.Validators[0].PubShares) require.NoError(t, err, "full deposit") } diff --git a/cmd/depositfetch.go b/cmd/depositfetch.go index 43910deef1..b92e816731 100644 --- a/cmd/depositfetch.go +++ b/cmd/depositfetch.go @@ -72,7 +72,20 @@ func runDepositFetch(ctx context.Context, config depositFetchConfig) error { for _, pubkey := range config.ValidatorPublicKeys { log.Info(ctx, "Fetching full deposit message", z.Str("validator_pubkey", pubkey)) - dd, err := oAPI.GetFullDeposit(ctx, pubkey, cl.LockHash, cl.Threshold) + var pubShares [][]byte + + for _, v := range cl.Validators { + if v.PublicKeyHex() == pubkey { + pubShares = v.PubShares + break + } + } + + if len(pubShares) == 0 { + return errors.New("validator public key not found in cluster lock", z.Str("validator_pubkey", pubkey)) + } + + dd, err := oAPI.GetFullDeposit(ctx, pubkey, cl.LockHash, cl.Threshold, pubShares) if err != nil { return errors.Wrap(err, "fetch full deposit data from Obol API") } diff --git a/eth2util/deposit/deposit.go b/eth2util/deposit/deposit.go index 077a8e8596..ffee33a09e 100644 --- a/eth2util/deposit/deposit.go +++ b/eth2util/deposit/deposit.go @@ -116,7 +116,14 @@ func MarshalDepositData(depositDatas []eth2p0.DepositData, network string) ([]by err = tbls.Verify(blsPubkey, sigData[:], blsSig) if err != nil { - return nil, errors.Wrap(err, "invalid deposit data signature") + return nil, errors.Wrap(err, "invalid deposit data signature", + z.Str("pubkey", hex.EncodeToString(depositData.PublicKey[:])), + z.Str("signature", hex.EncodeToString(depositData.Signature[:])), + z.Str("msg.pubkey", hex.EncodeToString(msg.PublicKey[:])), + z.Str("msg.withdrawal_credentials", hex.EncodeToString(msg.WithdrawalCredentials)), + z.U64("msg.amount", uint64(msg.Amount)), + z.Str("network", network), + ) } dataRoot, err := depositData.HashTreeRoot() diff --git a/testutil/obolapimock/deposit.go b/testutil/obolapimock/deposit.go index 9b480f75d9..e45389d25a 100644 --- a/testutil/obolapimock/deposit.go +++ b/testutil/obolapimock/deposit.go @@ -23,11 +23,9 @@ const ( fetchFullDepositTmpl = "/deposit_data/" + lockHashPath + "/" + valPubkeyPath ) -// depositBlob represents an Obol API DepositBlob with its share index. +// depositBlob represents an Obol API DepositBlob. type depositBlob struct { obolapi.FullDepositResponse - - shareIdx uint64 } func (ts *testServer) HandleSubmitPartialDeposit(writer http.ResponseWriter, request *http.Request) { @@ -112,9 +110,14 @@ func (ts *testServer) HandleSubmitPartialDeposit(writer http.ResponseWriter, req return } + newPartial := obolapi.Partial{ + PartialPublicKey: hex.EncodeToString(publicKeyShare[:]), + PartialDepositSignature: depositData.Signature.String(), + } + existingDeposit, ok := ts.partialDeposits[depositData.PublicKey.String()] - amounts := []obolapi.Amount{} + var amounts []obolapi.Amount if ok { amounts = existingDeposit.Amounts } @@ -123,36 +126,25 @@ func (ts *testServer) HandleSubmitPartialDeposit(writer http.ResponseWriter, req for idx, amt := range amounts { if amt.Amount == strconv.FormatUint(uint64(depositData.Amount), 10) { - amt.Partials = append(amt.Partials, obolapi.Partial{ - PartialDepositSignature: depositData.Signature.String(), - PartialPublicKey: "", - }) + amt.Partials = append(amt.Partials, newPartial) amounts[idx] = amt amtFound = true } } - existingDeposit.Amounts = amounts - if !amtFound { amounts = append(amounts, obolapi.Amount{ - Amount: strconv.FormatUint(uint64(depositData.Amount), 10), - Partials: []obolapi.Partial{ - { - PartialDepositSignature: depositData.Signature.String(), - PartialPublicKey: "", - }, - }, + Amount: strconv.FormatUint(uint64(depositData.Amount), 10), + Partials: []obolapi.Partial{newPartial}, }) + } - ts.partialDeposits[depositData.PublicKey.String()] = depositBlob{ - FullDepositResponse: obolapi.FullDepositResponse{ - PublicKey: depositData.PublicKey.String(), - WithdrawalCredentials: hex.EncodeToString(depositData.WithdrawalCredentials), - Amounts: amounts, - }, - shareIdx: shareIndex, - } + ts.partialDeposits[depositData.PublicKey.String()] = depositBlob{ + FullDepositResponse: obolapi.FullDepositResponse{ + PublicKey: depositData.PublicKey.String(), + WithdrawalCredentials: hex.EncodeToString(depositData.WithdrawalCredentials), + Amounts: amounts, + }, } }