Skip to content

Commit 425dbf8

Browse files
authored
Decouple storage: Validations + Delegations (#1200)
* Decouple storage from staker: validations+delegations * test hayabusa-e2e diff version
1 parent 429e5d4 commit 425dbf8

23 files changed

Lines changed: 1160 additions & 1032 deletions

.github/workflows/test-e2e.yaml

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,35 +51,36 @@ jobs:
5151
THOR_IMAGE: vechain/thor:${{ github.sha }}
5252
name: Run Tests
5353
steps:
54-
# Main: https://github.com/vechain/thor-e2e-tests/tree/062103aa04f9e30895ec97c8b37974ae2d99479c
54+
# Main: https://github.com/vechain/thor-e2e-tests/tree/650d77bcf8f6a2ebab1e5334c34917bfb2eebe9b
5555
# For release branches, names in the thor and thor-e2e-tests repos should match
5656
- name: Set e2e repo reference
5757
id: set-ref
5858
run: |
5959
# Default fallback commit
60-
DEFAULT_COMMIT="062103aa04f9e30895ec97c8b37974ae2d99479c"
60+
DEFAULT_COMMIT="650d77bcf8f6a2ebab1e5334c34917bfb2eebe9b"
6161
REF="$DEFAULT_COMMIT"
62-
63-
if [ "${{ github.event_name }}" = "pull_request" ]; then
62+
63+
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
6464
# For pull_request events, we only look at the PR's base branch
65-
if [[ "${{ github.event.pull_request.base.ref }}" == release/* ]]; then
66-
REF="${{ github.event.pull_request.base.ref }}"
65+
if [[ "$GITHUB_BASE_REF" == release/* ]]; then
66+
REF="$GITHUB_BASE_REF"
6767
fi
6868
else
6969
# For push events, we check the branch or tag that was pushed
70-
if [[ "${{ github.ref_name }}" == release/* ]]; then
71-
REF="${{ github.ref_name }}"
70+
if [[ "$GITHUB_REF_NAME" == release/* ]]; then
71+
REF="$GITHUB_REF_NAME"
7272
fi
7373
fi
74-
74+
7575
# Verify if branch exists
76-
if ! git ls-remote --heads https://github.com/vechain/thor-e2e-tests.git $REF; then
76+
if ! git ls-remote --heads https://github.com/vechain/thor-e2e-tests.git "$REF"; then
7777
echo "Branch does not exist, using default commit"
7878
echo "ref=$DEFAULT_COMMIT" >> "$GITHUB_OUTPUT"
7979
else
8080
echo "ref=$REF" >> "$GITHUB_OUTPUT"
8181
fi
8282
83+
8384
- name: Checkout
8485
uses: actions/checkout@v4
8586
with:

builtin/native_calls_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/vechain/thor/v2/block"
2525
"github.com/vechain/thor/v2/builtin"
2626
"github.com/vechain/thor/v2/builtin/staker"
27+
"github.com/vechain/thor/v2/builtin/staker/validation"
2728
"github.com/vechain/thor/v2/genesis"
2829
"github.com/vechain/thor/v2/muxdb"
2930
"github.com/vechain/thor/v2/runtime"
@@ -1667,7 +1668,7 @@ func TestStakerContract_Native(t *testing.T) {
16671668
assert.Equal(t, &expectedEndorsor, getRes[1])
16681669
assert.Equal(t, big.NewInt(0).Cmp(*getRes[2].(**big.Int)), 0) // stake - should be 0 while queued
16691670
assert.Equal(t, big.NewInt(0).Cmp(*getRes[3].(**big.Int)), 0) // weight - should be 0 while queued
1670-
assert.Equal(t, staker.StatusQueued, *getRes[4].(*uint8))
1671+
assert.Equal(t, validation.StatusQueued, *getRes[4].(*uint8))
16711672
assert.Equal(t, true, *getRes[5].(*bool)) // isMaster
16721673
assert.Equal(t, uint32(360*24*15), *getRes[6].(*uint32))
16731674
assert.Equal(t, uint32(0), *getRes[7].(*uint32)) // start period
@@ -1940,7 +1941,7 @@ func TestStakerContract_Native_WithdrawQueued(t *testing.T) {
19401941
getRes[8] = new(uint32)
19411942
_, err = callContractAndGetOutput(abi, "get", toAddr, &getRes, id)
19421943
assert.NoError(t, err)
1943-
assert.Equal(t, staker.StatusExit, *getRes[4].(*uint8))
1944+
assert.Equal(t, validation.StatusExit, *getRes[4].(*uint8))
19441945

19451946
// firstQueued
19461947
firstQueuedRes := new(common.Address)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) 2025 The VeChainThor developers
2+
//
3+
// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
4+
// file LICENSE or <https://www.gnu.org/licenses/lgpl-3.0.html>
5+
6+
package delegation
7+
8+
import (
9+
"math/big"
10+
11+
"github.com/vechain/thor/v2/builtin/staker/validation"
12+
"github.com/vechain/thor/v2/thor"
13+
)
14+
15+
type Delegation struct {
16+
ValidationID thor.Address // the ID of the validation to which the delegator is delegating
17+
Stake *big.Int
18+
Multiplier uint8
19+
LastIteration *uint32 `rlp:"nil"` // the last staking period in which the delegator was active
20+
FirstIteration uint32 // the iteration at which the delegator was created
21+
}
22+
23+
// IsEmpty returns whether the entry can be treated as empty.
24+
func (d *Delegation) IsEmpty() bool {
25+
return (d.Stake == nil || d.Stake.Sign() == 0) && d.Multiplier == 0
26+
}
27+
28+
// CalcWeight returns the weight of the delegator, which is calculated as:
29+
// weight = stake * multiplier / 100
30+
func (d *Delegation) CalcWeight() *big.Int {
31+
if d.IsEmpty() {
32+
return big.NewInt(0)
33+
}
34+
35+
weight := big.NewInt(0).Mul(d.Stake, big.NewInt(int64(d.Multiplier))) // multiplier is %
36+
weight = weight.Quo(weight, big.NewInt(100)) // convert to %
37+
38+
return weight
39+
}
40+
41+
// Started returns whether the delegation became locked
42+
func (d *Delegation) Started(val *validation.Validation) bool {
43+
if d.IsEmpty() {
44+
return false
45+
}
46+
if val.Status == validation.StatusQueued {
47+
return false // Delegation cannot start if the validation is not active
48+
}
49+
currentStakingPeriod := val.CurrentIteration()
50+
return currentStakingPeriod >= d.FirstIteration
51+
}
52+
53+
// Ended returns whether the delegation has ended
54+
// It returns true if:
55+
// - the delegation's exit iteration is less than the current staking period
56+
// - OR if the validation is in exit status and the delegation has started
57+
func (d *Delegation) Ended(val *validation.Validation) bool {
58+
if d.IsEmpty() {
59+
return false
60+
}
61+
if val.Status == validation.StatusQueued {
62+
return false // Delegation cannot end if the validation is not active
63+
}
64+
if val.Status == validation.StatusExit && d.Started(val) {
65+
return true // Delegation is ended if the validation is in exit status
66+
}
67+
currentStakingPeriod := val.CurrentIteration()
68+
if d.LastIteration == nil {
69+
return false
70+
}
71+
return *d.LastIteration < currentStakingPeriod
72+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) 2025 The VeChainThor developers
2+
//
3+
// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
4+
// file LICENSE or <https://www.gnu.org/licenses/lgpl-3.0.html>
5+
6+
package delegation
7+
8+
import (
9+
"math/big"
10+
11+
"github.com/pkg/errors"
12+
13+
"github.com/vechain/thor/v2/builtin/solidity"
14+
"github.com/vechain/thor/v2/builtin/staker/validation"
15+
"github.com/vechain/thor/v2/thor"
16+
)
17+
18+
var (
19+
slotDelegations = thor.BytesToBytes32([]byte(("delegations")))
20+
slotDelegationsCounter = thor.BytesToBytes32([]byte(("delegations-counter")))
21+
)
22+
23+
type Service struct {
24+
delegations *solidity.Mapping[thor.Bytes32, *Delegation]
25+
idCounter *solidity.Uint256
26+
}
27+
28+
func New(sctx *solidity.Context) *Service {
29+
return &Service{
30+
delegations: solidity.NewMapping[thor.Bytes32, *Delegation](sctx, slotDelegations),
31+
idCounter: solidity.NewUint256(sctx, slotDelegationsCounter),
32+
}
33+
}
34+
35+
func (s *Service) GetDelegation(delegationID thor.Bytes32) (*Delegation, error) {
36+
d, err := s.delegations.Get(delegationID)
37+
if err != nil {
38+
return nil, errors.Wrap(err, "failed to get delegation")
39+
}
40+
return d, nil
41+
}
42+
43+
func (s *Service) SetDelegation(delegationID thor.Bytes32, entry *Delegation, isNew bool) error {
44+
if err := s.delegations.Set(delegationID, entry, isNew); err != nil {
45+
return errors.Wrap(err, "failed to set delegation")
46+
}
47+
return nil
48+
}
49+
50+
func (s *Service) Add(
51+
validationID thor.Address,
52+
firstIteration uint32,
53+
stake *big.Int,
54+
multiplier uint8,
55+
) (thor.Bytes32, error) {
56+
// ensure input is sane
57+
if multiplier == 0 {
58+
return thor.Bytes32{}, errors.New("multiplier cannot be 0")
59+
}
60+
if stake.Cmp(big.NewInt(0)) <= 0 {
61+
return thor.Bytes32{}, errors.New("stake must be greater than 0")
62+
}
63+
64+
// update the global delegation counter
65+
// todo: should this counter be per validation ?
66+
id, err := s.idCounter.Get()
67+
if err != nil {
68+
return thor.Bytes32{}, err
69+
}
70+
71+
id = id.Add(id, big.NewInt(1))
72+
if err := s.idCounter.Set(id); err != nil {
73+
return thor.Bytes32{}, errors.Wrap(err, "failed to increment delegation ID counter")
74+
}
75+
76+
delegationID := thor.BytesToBytes32(id.Bytes())
77+
delegation := &Delegation{
78+
Multiplier: multiplier,
79+
Stake: stake,
80+
ValidationID: validationID,
81+
FirstIteration: firstIteration,
82+
}
83+
84+
if err := s.delegations.Set(delegationID, delegation, false); err != nil {
85+
return thor.Bytes32{}, errors.Wrap(err, "failed to set delegation")
86+
}
87+
88+
return delegationID, nil
89+
}
90+
91+
// todo uncouple this
92+
func (s *Service) SignalExit(delegationID thor.Bytes32, val *validation.Validation) error {
93+
delegation, err := s.GetDelegation(delegationID)
94+
if err != nil {
95+
return err
96+
}
97+
98+
if delegation.LastIteration != nil {
99+
return errors.New("delegation is already disabled for auto-renew")
100+
}
101+
if delegation.Stake.Sign() == 0 {
102+
return errors.New("delegation is not active")
103+
}
104+
if !delegation.Started(val) {
105+
return errors.New("delegation has not started yet, funds can be withdrawn")
106+
}
107+
if delegation.Ended(val) {
108+
return errors.New("delegation has ended, funds can be withdrawn")
109+
}
110+
111+
last := val.CurrentIteration()
112+
delegation.LastIteration = &last
113+
114+
return s.SetDelegation(delegationID, delegation, false)
115+
}
116+
117+
func (s *Service) Withdraw(delegationID thor.Bytes32) (*big.Int, *big.Int, error) {
118+
del, err := s.GetDelegation(delegationID)
119+
if err != nil {
120+
return nil, nil, err
121+
}
122+
123+
// ensure the pointers are copied, not referenced
124+
withdrawableStake := new(big.Int).Set(del.Stake)
125+
withdrawableStakeWeight := del.CalcWeight()
126+
127+
del.Stake = big.NewInt(0)
128+
if err := s.SetDelegation(delegationID, del, false); err != nil {
129+
return nil, nil, err
130+
}
131+
132+
return withdrawableStake, withdrawableStakeWeight, nil
133+
}

builtin/staker/delegations.go

Lines changed: 0 additions & 116 deletions
This file was deleted.

0 commit comments

Comments
 (0)