Skip to content

Commit aea1824

Browse files
authored
add remove dons changeset (smartcontractkit#16727)
1 parent ec846a2 commit aea1824

File tree

4 files changed

+647
-0
lines changed

4 files changed

+647
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package internal
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"math/big"
7+
8+
mcmstypes "github.com/smartcontractkit/mcms/types"
9+
10+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
11+
"github.com/smartcontractkit/chainlink/deployment"
12+
"github.com/smartcontractkit/chainlink/deployment/common/proposalutils"
13+
kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry_1_1_0"
14+
)
15+
16+
// RemoveDONsRequest holds the parameters for the RemoveDONs operation.
17+
type RemoveDONsRequest struct {
18+
Chain deployment.Chain
19+
CapabilitiesRegistry *kcr.CapabilitiesRegistry
20+
DONs []uint32
21+
UseMCMS bool
22+
// If UseMCMS is true and Ops is not nil then the RemoveDONs contract operation
23+
// will be added to the Ops.Batch.
24+
Ops *mcmstypes.BatchOperation
25+
}
26+
27+
// RemoveDONsResponse represents the response from calling RemoveDONs.
28+
type RemoveDONsResponse struct {
29+
// TxHash is the hash of the transaction if not using MCMS.
30+
TxHash string
31+
// Ops contains the MCMS operation (if any).
32+
Ops *mcmstypes.BatchOperation
33+
}
34+
35+
func (r *RemoveDONsRequest) Validate() error {
36+
if len(r.DONs) == 0 {
37+
return errors.New("DONs list is empty")
38+
}
39+
40+
if r.CapabilitiesRegistry == nil {
41+
return errors.New("registry is required")
42+
}
43+
return nil
44+
}
45+
46+
// RemoveDONs calls the RemoveDONs method on the capabilities registry contract.
47+
// It takes a list of DON IDs to remove.
48+
func RemoveDONs(lggr logger.Logger, req *RemoveDONsRequest) (*RemoveDONsResponse, error) {
49+
if err := req.Validate(); err != nil {
50+
return nil, fmt.Errorf("failed to validate request: %w", err)
51+
}
52+
53+
txOpts := req.Chain.DeployerKey
54+
for _, don := range req.DONs {
55+
if _, err := req.CapabilitiesRegistry.GetDON(nil, don); err != nil {
56+
return nil, fmt.Errorf("DON ID %d not found in registry: %w", don, err)
57+
}
58+
}
59+
60+
if req.UseMCMS {
61+
txOpts = deployment.SimTransactOpts()
62+
}
63+
64+
tx, err := req.CapabilitiesRegistry.RemoveDONs(txOpts, req.DONs)
65+
if err != nil {
66+
err = deployment.DecodeErr(kcr.CapabilitiesRegistryABI, err)
67+
return nil, fmt.Errorf("failed to call RemoveDONs: %w", err)
68+
}
69+
70+
var ops mcmstypes.BatchOperation
71+
if !req.UseMCMS {
72+
_, err = req.Chain.Confirm(tx)
73+
if err != nil {
74+
return nil, fmt.Errorf("failed to confirm RemoveDONs transaction %s: %w", tx.Hash().String(), err)
75+
}
76+
} else {
77+
ops, err = proposalutils.BatchOperationForChain(req.Chain.Selector, req.CapabilitiesRegistry.Address().Hex(), tx.Data(), big.NewInt(0), string(CapabilitiesRegistry), nil)
78+
if err != nil {
79+
return nil, fmt.Errorf("failed to create batch operation: %w", err)
80+
}
81+
}
82+
83+
return &RemoveDONsResponse{
84+
TxHash: tx.Hash().String(),
85+
Ops: &ops,
86+
}, nil
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package internal_test
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
mcmstypes "github.com/smartcontractkit/mcms/types"
13+
14+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
15+
"github.com/smartcontractkit/chainlink/deployment"
16+
"github.com/smartcontractkit/chainlink/deployment/data-streams/utils/pointer"
17+
"github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal"
18+
kstest "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/test"
19+
kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry_1_1_0"
20+
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey"
21+
)
22+
23+
func Test_RemoveDONsRequest_validate(t *testing.T) {
24+
type fields struct {
25+
DONs []uint32
26+
chain deployment.Chain
27+
capabilitiesRegistry *kcr.CapabilitiesRegistry
28+
}
29+
tests := []struct {
30+
name string
31+
fields fields
32+
wantErr bool
33+
}{
34+
{
35+
name: "missing capabilities registry",
36+
fields: fields{
37+
DONs: []uint32{1},
38+
chain: deployment.Chain{},
39+
capabilitiesRegistry: nil,
40+
},
41+
wantErr: true,
42+
},
43+
{
44+
name: "empty DONs list",
45+
fields: fields{
46+
DONs: []uint32{},
47+
chain: deployment.Chain{},
48+
capabilitiesRegistry: &kcr.CapabilitiesRegistry{},
49+
},
50+
wantErr: true,
51+
},
52+
{
53+
name: "success",
54+
fields: fields{
55+
DONs: []uint32{1},
56+
chain: deployment.Chain{},
57+
capabilitiesRegistry: &kcr.CapabilitiesRegistry{},
58+
},
59+
wantErr: false,
60+
},
61+
}
62+
for _, tt := range tests {
63+
t.Run(tt.name, func(t *testing.T) {
64+
req := &internal.RemoveDONsRequest{
65+
DONs: tt.fields.DONs,
66+
Chain: tt.fields.chain,
67+
CapabilitiesRegistry: tt.fields.capabilitiesRegistry,
68+
}
69+
if err := req.Validate(); (err != nil) != tt.wantErr {
70+
t.Errorf("internal.RemoveDONsRequest.validate() error = %v, wantErr %v", err, tt.wantErr)
71+
}
72+
})
73+
}
74+
}
75+
76+
// TestRemoveDONs tests the RemoveDONs function
77+
func TestRemoveDONs(t *testing.T) {
78+
t.Parallel()
79+
lggr := logger.Test(t)
80+
81+
var (
82+
p2p1 = p2pkey.MustNewV2XXXTestingOnly(big.NewInt(100))
83+
pubKey1 = "11114981a6119ca3f932cdb8c402d71a72d672adae7849f581ecff8b8e1098e7" // valid csa key
84+
admin1 = common.HexToAddress("0x1111567890123456789012345678901234567890") // valid eth address
85+
signing1 = "11117293a4cc2621b61193135a95928735e4795f" // valid eth address
86+
node1 = newNode(t, minimalNodeCfg{
87+
id: "test node 1",
88+
pubKey: pubKey1,
89+
registryChain: registryChain,
90+
p2p: p2p1,
91+
signingAddr: signing1,
92+
admin: admin1,
93+
})
94+
95+
p2p2 = p2pkey.MustNewV2XXXTestingOnly(big.NewInt(200))
96+
pubKey2 = "22224981a6119ca3f932cdb8c402d71a72d672adae7849f581ecff8b8e109000" // valid csa key
97+
admin2 = common.HexToAddress("0x2222567890123456789012345678901234567891") // valid eth address
98+
signing2 = "22227293a4cc2621b61193135a95928735e4ffff" // valid eth address
99+
node2 = newNode(t, minimalNodeCfg{
100+
id: "test node 2",
101+
pubKey: pubKey2,
102+
registryChain: registryChain,
103+
p2p: p2p2,
104+
signingAddr: signing2,
105+
admin: admin2,
106+
})
107+
108+
p2p3 = p2pkey.MustNewV2XXXTestingOnly(big.NewInt(300))
109+
pubKey3 = "33334981a6119ca3f932cdb8c402d71a72d672adae7849f581ecff8b8e109111" // valid csa key
110+
admin3 = common.HexToAddress("0x3333567890123456789012345678901234567892") // valid eth address
111+
signing3 = "33337293a4cc2621b61193135a959287aaaaffff" // valid eth address
112+
node3 = newNode(t, minimalNodeCfg{
113+
id: "test node 3",
114+
pubKey: pubKey3,
115+
registryChain: registryChain,
116+
p2p: p2p3,
117+
signingAddr: signing3,
118+
admin: admin3,
119+
})
120+
121+
p2p4 = p2pkey.MustNewV2XXXTestingOnly(big.NewInt(400))
122+
pubKey4 = "44444981a6119ca3f932cdb8c402d71a72d672adae7849f581ecff8b8e109222" // valid csa key
123+
admin4 = common.HexToAddress("0x4444567890123456789012345678901234567893") // valid eth address
124+
signing4 = "44447293a4cc2621b61193135a959287aaaaffff" // valid eth address
125+
node4 = newNode(t, minimalNodeCfg{
126+
id: "test node 4",
127+
pubKey: pubKey4,
128+
registryChain: registryChain,
129+
p2p: p2p4,
130+
signingAddr: signing4,
131+
admin: admin4,
132+
})
133+
134+
initialCap = kcr.CapabilitiesRegistryCapability{
135+
LabelledName: "test",
136+
Version: "1.0.0",
137+
CapabilityType: 0,
138+
}
139+
140+
initialCapCfg = kstest.GetDefaultCapConfig(t, initialCap)
141+
)
142+
143+
setupEnv := func(t *testing.T) (deployment.Chain, *kcr.CapabilitiesRegistry, []kcr.CapabilitiesRegistryDONInfo) {
144+
// 1) Set up chain + registry
145+
cfg := setupUpdateDonTestConfig{
146+
dons: []internal.DonInfo{
147+
{
148+
Name: "don 1",
149+
Nodes: []deployment.Node{node1, node2, node3, node4},
150+
Capabilities: []internal.DONCapabilityWithConfig{{Capability: initialCap, Config: initialCapCfg}},
151+
},
152+
},
153+
nops: []internal.NOP{
154+
{
155+
Name: "nop 1",
156+
Nodes: []string{node1.NodeID, node2.NodeID, node3.NodeID, node4.NodeID},
157+
},
158+
},
159+
}
160+
161+
testResp := registerTestDon(t, lggr, cfg)
162+
163+
// 2) Grab the existing DONs in the test registry
164+
dons, err := testResp.CapabilitiesRegistry.GetDONs(&bind.CallOpts{})
165+
require.NoError(t, err)
166+
require.NotZero(t, dons, "failed to find newly created DON")
167+
168+
return testResp.Chain, testResp.CapabilitiesRegistry, dons
169+
}
170+
171+
tests := []struct {
172+
name string
173+
useMCMS bool
174+
wantErr bool
175+
validateRemoval bool // if true, we will confirm on-chain that the DON is removed
176+
donToRemove *uint32
177+
}{
178+
{
179+
name: "remove single DON - no MCMS",
180+
useMCMS: false,
181+
wantErr: false,
182+
validateRemoval: true, // we finalize on-chain => check removal
183+
},
184+
{
185+
name: "remove single DON - with MCMS",
186+
useMCMS: true,
187+
wantErr: false,
188+
// In MCMS mode, the transaction is batched and not finalized on-chain,
189+
// so skip on-chain validation that it was actually removed.
190+
validateRemoval: false,
191+
},
192+
{
193+
name: "remove - error if DON not found in registry",
194+
useMCMS: false,
195+
wantErr: true,
196+
validateRemoval: false,
197+
donToRemove: pointer.To(uint32(2839748937)),
198+
},
199+
}
200+
201+
for _, tt := range tests {
202+
tt := tt
203+
t.Run(tt.name, func(t *testing.T) {
204+
t.Parallel()
205+
206+
chain, registry, dons := setupEnv(t)
207+
208+
donIDs := []uint32{dons[0].Id}
209+
210+
// Build the RemoveDONs request
211+
req := &internal.RemoveDONsRequest{
212+
Chain: chain,
213+
CapabilitiesRegistry: registry,
214+
DONs: donIDs,
215+
UseMCMS: tt.useMCMS,
216+
}
217+
if tt.useMCMS {
218+
req.Ops = &mcmstypes.BatchOperation{}
219+
}
220+
if tt.donToRemove != nil {
221+
req.DONs = []uint32{*tt.donToRemove}
222+
}
223+
224+
// Invoke RemoveDONs
225+
resp, err := internal.RemoveDONs(lggr, req)
226+
227+
if tt.wantErr {
228+
require.Error(t, err)
229+
return
230+
}
231+
require.NoError(t, err, "RemoveDONs should not fail in this scenario")
232+
require.NotNil(t, resp)
233+
assert.NotEmpty(t, resp.TxHash, "Expected a valid TxHash")
234+
235+
if tt.useMCMS {
236+
require.NotNil(t, resp.Ops, "Ops should be set in MCMS mode")
237+
require.NotEmpty(t, resp.Ops.Transactions, "Expected at least one transaction in MCMS batch")
238+
} else {
239+
assert.Empty(t, resp.Ops.Transactions, "No MCMS operation should be returned if not in MCMS mode")
240+
}
241+
242+
// Optionally confirm the removal on-chain if this test scenario does a direct finalization
243+
if tt.validateRemoval {
244+
don, err := registry.GetDON(&bind.CallOpts{}, dons[0].Id)
245+
require.NoError(t, err)
246+
require.NotEqual(t, dons[0].Id, don.Id, "Expect donID to not be found")
247+
require.Equal(t, uint32(0), don.Id, "Expected returned donID to be 0")
248+
}
249+
})
250+
}
251+
}

0 commit comments

Comments
 (0)