Skip to content

Commit

Permalink
deploy: Fix NEO transfer deadlock caused by rounding integer division
Browse files Browse the repository at this point in the history
Previously, auto-deploy/update procedure could stuck when committee
multi-sig account had less than N amount of NEO, where N is a number of
the NeoFS Alphabet accounts in the deployed/update NeoFS network.
This was caused by rounding integer division of fund amounts: zero funds
were transferred due to which the balance did not change and each
iteration did not change the network state.

Fix zero transfers and also distribute remainder as evenly as possible
to decrease total number of transactions.

Fixes #2681.

Signed-off-by: Leonard Lyubich <leonard@morphbits.io>
  • Loading branch information
cthulhu-rider committed Dec 18, 2023
1 parent 005584e commit 5b80a03
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 7 deletions.
52 changes: 45 additions & 7 deletions pkg/morph/deploy/funds.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,16 +414,13 @@ func distributeNEOToAlphabetContracts(ctx context.Context, prm distributeNEOToAl
prm.logger.Info("have available NEO on the committee multi-sig account, going to transfer to the Alphabet contracts",
zap.Stringer("balance", bal))

singleAmount := new(big.Int).Div(bal, big.NewInt(int64(len(prm.alphabetContracts))))

scriptBuilder.Reset()

for i := range prm.alphabetContracts {
divideFundsEvenly(bal, len(prm.alphabetContracts), func(i int, amount *big.Int) {
prm.logger.Info("going to transfer NEO from the committee multi-sig account to the Alphabet contract",
zap.Stringer("contract", prm.alphabetContracts[i]), zap.Stringer("amount", singleAmount))
transfer(prm.alphabetContracts[i], singleAmount)
bal.Sub(bal, singleAmount)
}
zap.Stringer("contract", prm.alphabetContracts[i]), zap.Stringer("amount", amount))
transfer(prm.alphabetContracts[i], amount)
})

script, err := scriptBuilder.Script()
if err != nil {
Expand All @@ -448,3 +445,44 @@ func distributeNEOToAlphabetContracts(ctx context.Context, prm distributeNEOToAl
txMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID)
}
}

func divideFundsEvenly(fullAmount *big.Int, n int, f func(ind int, amount *big.Int)) {
if fullAmount.IsUint64() {
divideFundsEvenlyU64(fullAmount.Uint64(), n, func(ind int, amount uint64) {
f(ind, new(big.Int).SetUint64(amount))
})
return
}

quot := new(big.Int).Div(fullAmount, big.NewInt(int64(n)))
rem := new(big.Int).Mod(fullAmount, big.NewInt(int64(n)))

for i := 0; i < n; i++ {
amount := new(big.Int).Set(quot)
if rem.Sign() > 0 {
amount.Add(amount, big.NewInt(1))
rem.Sub(rem, big.NewInt(1))
} else if amount.Sign() == 0 {
return
}

f(i, amount)
}
}

func divideFundsEvenlyU64(fullAmount uint64, n int, f func(ind int, amount uint64)) {
quot := fullAmount / uint64(n)
rem := fullAmount % uint64(n)

for i := 0; i < n; i++ {
amount := quot
if rem > 0 {
amount++
rem--
} else if amount == 0 {
return
}

f(i, amount)
}
}
83 changes: 83 additions & 0 deletions pkg/morph/deploy/funds_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package deploy

import (
"math"
"math/big"
"testing"

"github.com/stretchr/testify/require"
)

func TestDivideFundsEvenly(t *testing.T) {
t.Run("zero", func(t *testing.T) {
var vals []*big.Int

divideFundsEvenly(big.NewInt(0), 5, func(ind int, amount *big.Int) {
vals = append(vals, amount)
})
require.Empty(t, vals)
})

t.Run("less than N", func(t *testing.T) {
var vals []*big.Int

divideFundsEvenly(big.NewInt(4), 5, func(ind int, amount *big.Int) {
vals = append(vals, amount)
})
require.Len(t, vals, 4)
for i := range vals {
require.Equal(t, big.NewInt(1), vals[i])
}
})

t.Run("multiple", func(t *testing.T) {
var vals []*big.Int

divideFundsEvenly(big.NewInt(15), 3, func(ind int, amount *big.Int) {
vals = append(vals, amount)
})
require.Len(t, vals, 3)
for i := range vals {
require.Equal(t, big.NewInt(5), vals[i])
}
})

t.Run("with remainder", func(t *testing.T) {
var vals []*big.Int

divideFundsEvenly(big.NewInt(16), 3, func(ind int, amount *big.Int) {
vals = append(vals, amount)
})
require.Len(t, vals, 3)
require.Equal(t, big.NewInt(6), vals[0])
require.Equal(t, big.NewInt(5), vals[1])
require.Equal(t, big.NewInt(5), vals[2])
})

t.Run("bigger than uint64", func(t *testing.T) {
bigU64 := new(big.Int).SetUint64(math.MaxUint64)
fullAmount := new(big.Int).Mul(bigU64, big.NewInt(3))

var vals []*big.Int

divideFundsEvenly(fullAmount, 3, func(ind int, amount *big.Int) {
vals = append(vals, amount)
})
require.Len(t, vals, 3)
for i := range vals {
require.Equal(t, bigU64, vals[i])
}
})
}

func BenchmarkDivideFundsEvenly(b *testing.B) {
const n = 7
amount := big.NewInt(100*n + n/2)

b.ReportAllocs()
b.ResetTimer()

for i := 0; i < b.N; i++ {
divideFundsEvenly(amount, n, func(ind int, amount *big.Int) {})
}
}

0 comments on commit 5b80a03

Please sign in to comment.