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 7651077
Show file tree
Hide file tree
Showing 2 changed files with 142 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) {

Check warning on line 419 in pkg/morph/deploy/funds.go

View check run for this annotation

Codecov / codecov/patch

pkg/morph/deploy/funds.go#L419

Added line #L419 was not covered by tests
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)
})

Check warning on line 423 in pkg/morph/deploy/funds.go

View check run for this annotation

Codecov / codecov/patch

pkg/morph/deploy/funds.go#L421-L423

Added lines #L421 - L423 were not covered by tests

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))

Check warning on line 464 in pkg/morph/deploy/funds.go

View check run for this annotation

Codecov / codecov/patch

pkg/morph/deploy/funds.go#L463-L464

Added lines #L463 - L464 were not covered by tests
} else if amount.Sign() == 0 {
return

Check warning on line 466 in pkg/morph/deploy/funds.go

View check run for this annotation

Codecov / codecov/patch

pkg/morph/deploy/funds.go#L466

Added line #L466 was not covered by tests
}

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)
}
}
97 changes: 97 additions & 0 deletions pkg/morph/deploy/funds_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package deploy

import (
"fmt"
"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) {
for _, tc := range []struct {
n int
a *big.Int
}{
{
n: 7,
a: big.NewInt(705),
},
{
n: 100,
a: new(big.Int).Mul(new(big.Int).SetUint64(math.MaxUint64), big.NewInt(255)),
},
} {
b.Run(fmt.Sprintf("N=%d,amount=%s", tc.n, tc.a), func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()

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

0 comments on commit 7651077

Please sign in to comment.