Skip to content

Commit

Permalink
chore: refactory sanitize delegations
Browse files Browse the repository at this point in the history
  • Loading branch information
RafilxTenfen committed Jan 10, 2025
1 parent ed1ff7a commit 1b03563
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 121 deletions.
83 changes: 50 additions & 33 deletions covenant/cache_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,55 +9,72 @@ import (
"go.uber.org/zap"
)

type ParamsGetter interface {
Get(version uint32) (*types.StakingParams, error)
}
type (
getParamByVersion func(version uint32) (*types.StakingParams, error)
ParamsGetter interface {
Get(version uint32) (*types.StakingParams, error)
}
CacheVersionedParams struct {
sync.Mutex
paramsByVersion map[uint32]*types.StakingParams

type VersionedParams struct {
sync.Mutex
paramsByVersion map[uint32]*types.StakingParams
cc clientcontroller.ClientController
logger *zap.Logger
}
getParamsByVersion getParamByVersion
}
)

func NewCacheVersionedParams(cc clientcontroller.ClientController, logger *zap.Logger) ParamsGetter {
return &VersionedParams{
paramsByVersion: make(map[uint32]*types.StakingParams),
cc: cc,
logger: logger,
func NewCacheVersionedParams(f getParamByVersion) ParamsGetter {
return &CacheVersionedParams{
paramsByVersion: make(map[uint32]*types.StakingParams),
getParamsByVersion: f,
}
}

func (v *VersionedParams) Get(version uint32) (*types.StakingParams, error) {
// Get returns the staking parameter from the
func (v *CacheVersionedParams) Get(version uint32) (*types.StakingParams, error) {
v.Lock()
defer v.Unlock()

var (
err error
)

params, ok := v.paramsByVersion[version]
if ok {
return params, nil
}

if err := retry.Do(func() error {
params, err = v.cc.QueryStakingParamsByVersion(version)
if err != nil {
return err
}
return nil
}, RtyAtt, RtyDel, RtyErr, retry.OnRetry(func(n uint, err error) {
v.logger.Debug(
"failed to query the consumer chain for the staking params",
zap.Uint("attempt", n+1),
zap.Uint("max_attempts", RtyAttNum),
zap.Error(err),
)
})); err != nil {
params, err := v.getParamsByVersion(version)
if err != nil {
return nil, err
}

v.paramsByVersion[version] = params
return params, nil
}

func paramsByVersion(
cc clientcontroller.ClientController,
logger *zap.Logger,
) getParamByVersion {
return func(version uint32) (*types.StakingParams, error) {
var (
err error
params *types.StakingParams
)

if err := retry.Do(func() error {
params, err = cc.QueryStakingParamsByVersion(version)
if err != nil {
return err
}
return nil
}, RtyAtt, RtyDel, RtyErr, retry.OnRetry(func(n uint, err error) {
logger.Debug(
"failed to query the consumer chain for the staking params",
zap.Uint("attempt", n+1),
zap.Uint("max_attempts", RtyAttNum),
zap.Error(err),
)
})); err != nil {
return nil, err
}

return params, nil
}
}
70 changes: 35 additions & 35 deletions covenant/covenant.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,15 @@ func NewCovenantEmulator(
return nil, fmt.Errorf("failed to get signer pub key: %w", err)
}

paramGetter := paramsByVersion(cc, logger)
return &CovenantEmulator{
cc: cc,
signer: signer,
config: config,
logger: logger,
pk: pk,
quit: make(chan struct{}),
paramCache: NewCacheVersionedParams(cc, logger.With(zap.String("cache", "VersionedParams"))),
paramCache: NewCacheVersionedParams(paramGetter),
}, nil
}

Expand Down Expand Up @@ -103,8 +104,7 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) (
}

// 1. get the params matched to the delegation version
// TODO: add cache for get params
params, err := ce.getParamsByVersion(btcDel.ParamsVersion)
params, err := ce.paramCache.Get(btcDel.ParamsVersion)
if err != nil {
return nil, fmt.Errorf("failed to get staking params with version %d: %w", btcDel.ParamsVersion, err)
}
Expand Down Expand Up @@ -440,27 +440,28 @@ func (ce *CovenantEmulator) delegationsToBatches(dels []*types.Delegation) [][]*
return batches
}

func RemoveNotInCommittee(paramCache ParamsGetter, localKey *btcec.PublicKey, dels []*types.Delegation) []*types.Delegation {
func RemoveNotInCommittee(paramCache ParamsGetter, covenantSerializedPk []byte, dels []*types.Delegation) []*types.Delegation {
sanitized := make([]*types.Delegation, 0, len(dels))
localKeyBytes := schnorr.SerializePubKey(localKey)

for _, del := range dels {
if !IsKeyInCommittee(paramCache, localKeyBytes, del) {
if !IsKeyInCommittee(paramCache, covenantSerializedPk, del) {
continue
}
sanitized = append(sanitized, del)
}
return sanitized
}

func IsKeyInCommittee(paramCache ParamsGetter, localKeyBytes []byte, del *types.Delegation) bool {
// IsKeyInCommittee verifies
func IsKeyInCommittee(paramCache ParamsGetter, covenantSerializedPk []byte, del *types.Delegation) bool {
stkParams, err := paramCache.Get(del.ParamsVersion)
if err != nil {
return false
}

for _, pk := range stkParams.CovenantPks {
remoteKey := schnorr.SerializePubKey(pk)
if !bytes.Equal(remoteKey, localKeyBytes) {
if !bytes.Equal(remoteKey, covenantSerializedPk) {
continue
}
return true
Expand All @@ -469,35 +470,40 @@ func IsKeyInCommittee(paramCache ParamsGetter, localKeyBytes []byte, del *types.
return false
}

func RemoveAlreadySigned(localKey *btcec.PublicKey, dels []*types.Delegation) []*types.Delegation {
func RemoveAlreadySigned(covenantSerializedPk []byte, dels []*types.Delegation) []*types.Delegation {
sanitized := make([]*types.Delegation, 0, len(dels))
localKeyBytes := schnorr.SerializePubKey(localKey)

for _, del := range dels {
delCopy := del
alreadySigned := false
for _, covSig := range delCopy.CovenantSigs {
remoteKey := schnorr.SerializePubKey(covSig.Pk)
if bytes.Equal(remoteKey, localKeyBytes) {
alreadySigned = true
break
}
}
if !alreadySigned {
sanitized = append(sanitized, delCopy)
if CovenantAlreadySigned(covenantSerializedPk, del) {
continue
}
sanitized = append(sanitized, del)
}

return sanitized
}

// removeAlreadySigned removes any delegations that have already been signed by the covenant
func (ce *CovenantEmulator) removeAlreadySigned(dels []*types.Delegation) []*types.Delegation {
return RemoveAlreadySigned(ce.pk, dels)
// CovenantAlreadySigned returns true if the covenant already signed the BTC Delegation
func CovenantAlreadySigned(covenantSerializedPk []byte, del *types.Delegation) bool {
for _, covSig := range del.CovenantSigs {
remoteKey := schnorr.SerializePubKey(covSig.Pk)
if !bytes.Equal(remoteKey, covenantSerializedPk) {
continue
}
return true
}

return false
}

// removeNotInCommittee removes any delegations that have already been signed by the covenant
func (ce *CovenantEmulator) removeNotInCommittee(dels []*types.Delegation) []*types.Delegation {
return RemoveNotInCommittee(ce.paramCache, ce.pk, dels)
// sanitizeDelegations removes any delegations that have already been signed by the covenant and
// remove delegations that were not constructed with this covenant public key
func (ce *CovenantEmulator) sanitizeDelegations(dels []*types.Delegation) []*types.Delegation {
covenantSerializedPk := schnorr.SerializePubKey(ce.pk)
// 1. Remove delegations that do not need the covenant's signature
delsNotSigned := RemoveAlreadySigned(covenantSerializedPk, dels)
// 2. Remove delegations that were not constructed with this covenant public key
return RemoveNotInCommittee(ce.paramCache, covenantSerializedPk, delsNotSigned)
}

// covenantSigSubmissionLoop is the reactor to submit Covenant signature for BTC delegations
Expand Down Expand Up @@ -528,12 +534,10 @@ func (ce *CovenantEmulator) covenantSigSubmissionLoop() {
ce.logger.Debug("no pending delegations are found")
}
// 2. Remove delegations that do not need the covenant's signature
delsNotSigned := ce.removeAlreadySigned(dels)
// 3. Remove delegations that were not constructed with this covenant public key
delsToSign := ce.removeNotInCommittee(delsNotSigned)
sanitized := ce.sanitizeDelegations(dels)

// 3. Split delegations into batches for submission
batches := ce.delegationsToBatches(delsToSign)
batches := ce.delegationsToBatches(sanitized)
for _, delBatch := range batches {
_, err := ce.AddCovenantSignatures(delBatch)
if err != nil {
Expand Down Expand Up @@ -572,10 +576,6 @@ func (ce *CovenantEmulator) metricsUpdateLoop() {
}
}

func (ce *CovenantEmulator) getParamsByVersion(version uint32) (*types.StakingParams, error) {
return ce.paramCache.Get(version)
}

func (ce *CovenantEmulator) recordMetricsFailedSignDelegations(n int) {
failedSignDelegations.WithLabelValues(ce.PublicKeyStr()).Add(float64(n))
}
Expand Down
85 changes: 32 additions & 53 deletions covenant/covenant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,16 +246,12 @@ func TestDeduplicationWithOddKey(t *testing.T) {
}

// 4. After removing the already signed delegation, the list should have only one element
sanitized := covenant.RemoveAlreadySigned(oddKeyPub, delegations)
sanitized := covenant.RemoveAlreadySigned(schnorr.SerializePubKey(oddKeyPub), delegations)
require.Equal(t, 1, len(sanitized))
}

func TestIsKeyInCommittee(t *testing.T) {
r := rand.New(rand.NewSource(time.Now().Unix()))
delSK, delPK, err := datagen.GenRandomBTCKeyPair(r)
require.NoError(t, err)

params := testutil.GenRandomParams(r, t)

// create a Covenant key pair in the keyring
covenantConfig := covcfg.DefaultConfig()
Expand All @@ -270,62 +266,45 @@ func TestIsKeyInCommittee(t *testing.T) {
hdPath,
)
require.NoError(t, err)
covenantSerializedPk := schnorr.SerializePubKey(covKeyPair.PublicKey)

// creates one delegation to check
stakingTimeBlocks := uint32(testutil.RandRange(r, int(params.MinStakingTime), int(params.MaxStakingTime)))
stakingValue := int64(testutil.RandRange(r, int(params.MinStakingValue), int(params.MaxStakingValue)))
unbondingTime := uint16(params.UnbondingTimeBlocks)
fpNum := datagen.RandomInt(r, 5) + 1
fpPks := testutil.GenBtcPublicKeys(r, t, int(fpNum))
testInfo := datagen.GenBTCStakingSlashingInfo(
r,
t,
net,
delSK,
fpPks,
params.CovenantPks,
params.CovenantQuorum,
uint16(stakingTimeBlocks),
stakingValue,
params.SlashingPkScript,
params.SlashingRate,
unbondingTime,
)
stakingTxBytes, err := bbntypes.SerializeBTCTx(testInfo.StakingTx)
require.NoError(t, err)
startHeight := uint32(datagen.RandomInt(r, 1000) + 100)
stakingOutputIdx, err := bbntypes.GetOutputIdxInBTCTx(testInfo.StakingTx, testInfo.StakingInfo.StakingOutput)
require.NoError(t, err)
randParamsVersion := uint32(datagen.RandomInRange(r, 1, 10))
btcDel := &types.Delegation{
BtcPk: delPK,
FpBtcPks: fpPks,
StakingTime: stakingTimeBlocks,
StartHeight: startHeight, // not relevant here
EndHeight: startHeight + stakingTimeBlocks,
TotalSat: btcutil.Amount(stakingValue),
UnbondingTime: unbondingTime,
StakingTxHex: hex.EncodeToString(stakingTxBytes),
StakingOutputIdx: stakingOutputIdx,
SlashingTxHex: testInfo.SlashingTx.ToHexStr(),
ParamsVersion: randParamsVersion,
// create params and version
pVersionWithoutCovenant := uint32(datagen.RandomInRange(r, 1, 10))
pVersionWithCovenant := pVersionWithoutCovenant + 1

paramsWithoutCovenant := testutil.GenRandomParams(r, t)
paramsWithCovenant := testutil.GenRandomParams(r, t)
paramsWithCovenant.CovenantPks = append(paramsWithCovenant.CovenantPks, covKeyPair.PublicKey)

// creates delegations to check
delNoCovenant := &types.Delegation{
ParamsVersion: pVersionWithoutCovenant,
}
delWithCovenant := &types.Delegation{
ParamsVersion: pVersionWithCovenant,
}

paramsWithoutCovenant := NewMockParam(map[uint32]*types.StakingParams{
randParamsVersion: params,
// simple mock with the parameter versions
paramsGet := NewMockParam(map[uint32]*types.StakingParams{
pVersionWithoutCovenant: paramsWithoutCovenant,
pVersionWithCovenant: paramsWithCovenant,
})

localKeyBytes := schnorr.SerializePubKey(covKeyPair.PublicKey)
actual := covenant.IsKeyInCommittee(paramsWithoutCovenant, localKeyBytes, btcDel)
// checks the case where the covenant is NOT in the committee
actual := covenant.IsKeyInCommittee(paramsGet, covenantSerializedPk, delNoCovenant)
require.False(t, actual)
emptyDels := covenant.RemoveNotInCommittee(paramsGet, covenantSerializedPk, []*types.Delegation{delNoCovenant, delNoCovenant})
require.Len(t, emptyDels, 0)

// adds the covenant to the list in the params
params.CovenantPks = append(params.CovenantPks, covKeyPair.PublicKey)
paramsWithCovenant := NewMockParam(map[uint32]*types.StakingParams{
randParamsVersion: params,
})
actual = covenant.IsKeyInCommittee(paramsWithCovenant, localKeyBytes, btcDel)
// checks the case where the covenant is in the committee
actual = covenant.IsKeyInCommittee(paramsGet, covenantSerializedPk, delWithCovenant)
require.True(t, actual)
dels := covenant.RemoveNotInCommittee(paramsGet, covenantSerializedPk, []*types.Delegation{delWithCovenant, delNoCovenant})
require.Len(t, dels, 1)
dels = covenant.RemoveNotInCommittee(paramsGet, covenantSerializedPk, []*types.Delegation{delWithCovenant})
require.Len(t, dels, 1)
dels = covenant.RemoveNotInCommittee(paramsGet, covenantSerializedPk, []*types.Delegation{delWithCovenant, delWithCovenant, delNoCovenant})
require.Len(t, dels, 2)
}

type MockParamGetter struct {
Expand Down

0 comments on commit 1b03563

Please sign in to comment.