Skip to content

Commit 3f37c0d

Browse files
committed
feat: add governance-controlled machine whitelist system
Implements machine whitelist management through governance proposals with antehandler validation. Machine IDs must be exactly 20 bytes and validated against passed governance proposals. Changes: - Add MsgUpdateMachineWhitelistProposal for governance proposals - Add MsgUpdateMachineWhitelist for executing approved whitelists - Implement antehandler validation in CountTXDecorator - Queries governance module for proposal status - Validates machine IDs match proposal exactly (order preserved) - Rejects transactions if proposal not passed or IDs mismatch - Add CLI command: update-machine-whitelist [proposal-id] [machine-ids-file] - Supports JSON file format with hex-encoded machine IDs - Validates each ID is 20 bytes
1 parent 498c357 commit 3f37c0d

File tree

7 files changed

+1233
-101
lines changed

7 files changed

+1233
-101
lines changed

proto/secret/compute/v1beta1/msg.proto

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ service Msg {
3131
rpc UpgradeProposalPassed(MsgUpgradeProposalPassed) returns (MsgUpgradeProposalPassedResponse);
3232
rpc ContractGovernanceProposal(MsgContractGovernanceProposal) returns (MsgContractGovernanceProposalResponse);
3333
rpc SetContractGovernance(MsgSetContractGovernance) returns (MsgSetContractGovernanceResponse);
34+
rpc UpdateMachineWhitelistProposal(MsgUpdateMachineWhitelistProposal) returns (MsgUpdateMachineWhitelistProposalResponse);
35+
rpc UpdateMachineWhitelist(MsgUpdateMachineWhitelist) returns (MsgUpdateMachineWhitelistResponse);
3436
}
3537

3638
message MsgStoreCode {
@@ -252,4 +254,30 @@ message MsgSetContractGovernance {
252254
string contract_address = 2;
253255
}
254256

255-
message MsgSetContractGovernanceResponse {}
257+
message MsgSetContractGovernanceResponse {}
258+
259+
message MsgUpdateMachineWhitelistProposal {
260+
option (gogoproto.goproto_getters) = false;
261+
option (cosmos.msg.v1.signer) = "authority";
262+
option (amino.name) = "wasm/MsgUpdateMachineWhitelistProposal";
263+
264+
string authority = 1;
265+
string title = 2;
266+
string description = 3;
267+
repeated bytes machine_ids = 4;
268+
}
269+
270+
message MsgUpdateMachineWhitelistProposalResponse {}
271+
272+
// Execution message - sent by anyone after proposal passes
273+
message MsgUpdateMachineWhitelist {
274+
option (gogoproto.goproto_getters) = false;
275+
option (cosmos.msg.v1.signer) = "sender";
276+
option (amino.name) = "wasm/MsgUpdateMachineWhitelist";
277+
278+
string sender = 1;
279+
uint64 proposal_id = 2;
280+
repeated bytes machine_ids = 3;
281+
}
282+
283+
message MsgUpdateMachineWhitelistResponse {}

x/compute/client/cli/tx.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"context"
55
"encoding/hex"
6+
"encoding/json"
67
"fmt"
78
"os"
89
"strconv"
@@ -644,3 +645,80 @@ Examples:
644645
flags.AddTxFlagsToCmd(cmd)
645646
return cmd
646647
}
648+
649+
func UpdateMachineWhitelistCmd() *cobra.Command {
650+
cmd := &cobra.Command{
651+
Use: "update-machine-whitelist [proposal-id] [machine-ids-file]",
652+
Short: "Update machine whitelist after governance approval",
653+
Long: `Execute machine whitelist update after governance proposal passes.
654+
Machine IDs must match the approved proposal exactly.
655+
656+
Machine IDs file format (JSON):
657+
{
658+
"machine_ids": [
659+
"01507c9577896bc1afde972d67f1fd53af1a8da",
660+
"a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6"
661+
]
662+
}
663+
664+
Each machine ID must be exactly 20 bytes.`,
665+
Args: cobra.ExactArgs(2),
666+
RunE: func(cmd *cobra.Command, args []string) error {
667+
clientCtx, err := client.GetClientTxContext(cmd)
668+
if err != nil {
669+
return err
670+
}
671+
672+
proposalID, err := strconv.ParseUint(args[0], 10, 64)
673+
if err != nil {
674+
return fmt.Errorf("invalid proposal ID: %w", err)
675+
}
676+
677+
// Read machine IDs from file
678+
machineIDs, err := readMachineIDsFromFile(args[1])
679+
if err != nil {
680+
return err
681+
}
682+
683+
msg := &types.MsgUpdateMachineWhitelist{
684+
Sender: clientCtx.GetFromAddress().String(),
685+
ProposalId: proposalID,
686+
MachineIds: machineIDs,
687+
}
688+
689+
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
690+
},
691+
}
692+
693+
flags.AddTxFlagsToCmd(cmd)
694+
return cmd
695+
}
696+
697+
func readMachineIDsFromFile(filename string) ([][]byte, error) {
698+
data, err := os.ReadFile(filename)
699+
if err != nil {
700+
return nil, fmt.Errorf("failed to read file: %w", err)
701+
}
702+
703+
var config struct {
704+
MachineIDs []string `json:"machine_ids"`
705+
}
706+
707+
if err := json.Unmarshal(data, &config); err != nil {
708+
return nil, fmt.Errorf("failed to parse JSON: %w", err)
709+
}
710+
711+
machineIDs := make([][]byte, len(config.MachineIDs))
712+
for i, idHex := range config.MachineIDs {
713+
id, err := hex.DecodeString(idHex)
714+
if err != nil {
715+
return nil, fmt.Errorf("invalid hex at index %d: %w", i, err)
716+
}
717+
if len(id) != 20 {
718+
return nil, fmt.Errorf("machine ID at index %d must be 20 bytes, got %d", i, len(id))
719+
}
720+
machineIDs[i] = id
721+
}
722+
723+
return machineIDs, nil
724+
}

x/compute/internal/keeper/ante.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ type CountTXDecorator struct {
2626
storeService store.KVStoreService
2727
}
2828

29-
const msgSoftwareUpgradeTypeURL = "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade"
29+
const (
30+
msgSoftwareUpgradeTypeURL = "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade"
31+
msgUpdateMachineWhitelistProposalTypeURL = "/secret.compute.v1beta1.MsgUpdateMachineWhitelistProposal"
32+
)
3033

3134
// NewCountTXDecorator constructor
3235
func NewCountTXDecorator(appcodec codec.Codec, govkeeper govkeeper.Keeper, storeService store.KVStoreService) *CountTXDecorator {
@@ -96,6 +99,14 @@ func (a CountTXDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool,
9699
return ctx, err
97100
}
98101
}
102+
msgUpdateWhitelist, ok := msg.(*types.MsgUpdateMachineWhitelist)
103+
if ok {
104+
err = a.validateUpdateMachineWhitelist(ctx, msgUpdateWhitelist)
105+
if err != nil {
106+
ctx.Logger().Error("*** update machine whitelist rejected: ", err.Error())
107+
return ctx, err
108+
}
109+
}
99110
}
100111

101112
return next(types.WithTXCounter(ctx, txCounter), tx, simulate)
@@ -113,6 +124,41 @@ func extractInfoFromProposalMessages(message *types1.Any, cdc codec.Codec) (stri
113124
return softwareUpgradeMsg.Plan.Info, nil
114125
}
115126

127+
func (a *CountTXDecorator) validateUpdateMachineWhitelist(ctx sdk.Context, msgUpdateWhitelist *types.MsgUpdateMachineWhitelist) error {
128+
proposal, err := a.govkeeper.Proposals.Get(ctx, msgUpdateWhitelist.ProposalId) // just to ensure the proposal exists
129+
if err != nil {
130+
ctx.Logger().Error("*** proposal with such id %d not found: ", msgUpdateWhitelist.ProposalId, err.Error())
131+
return err
132+
}
133+
if proposal.Status != govtypes.ProposalStatus_PROPOSAL_STATUS_PASSED {
134+
return sdkerrors.ErrInvalidRequest.Wrapf("proposal with id %d not passed", msgUpdateWhitelist.ProposalId)
135+
}
136+
if len(proposal.Messages) != 1 {
137+
return sdkerrors.ErrInvalidRequest.Wrapf("proposal with id %d has %d messages, expected exactly 1", msgUpdateWhitelist.ProposalId, len(proposal.Messages))
138+
}
139+
if proposal.Messages[0].GetTypeUrl() != msgUpdateMachineWhitelistProposalTypeURL {
140+
return sdkerrors.ErrInvalidRequest.Wrapf("proposal with id %d is not of type MsgUpdateMachineWhitelist", msgUpdateWhitelist.ProposalId)
141+
}
142+
143+
var updateMachineWhitelistProposalMsg *types.MsgUpdateMachineWhitelistProposal
144+
err = a.appcodec.UnpackAny(proposal.Messages[0], &updateMachineWhitelistProposalMsg)
145+
if err != nil {
146+
ctx.Logger().Error("*** failed to unpack UpdateMachineWhitelist proposal message: ", err.Error())
147+
return err
148+
}
149+
150+
if len(msgUpdateWhitelist.MachineIds) != len(updateMachineWhitelistProposalMsg.MachineIds) {
151+
return sdkerrors.ErrInvalidRequest.Wrapf("machine ids count %d does not match the proposal %d", len(msgUpdateWhitelist.MachineIds), len(updateMachineWhitelistProposalMsg.MachineIds))
152+
}
153+
// ensure the machine ids match the proposal exactly in order
154+
for i, mid := range msgUpdateWhitelist.MachineIds {
155+
if !bytes.Equal(mid, updateMachineWhitelistProposalMsg.MachineIds[i]) {
156+
return sdkerrors.ErrInvalidRequest.Wrapf("machine id %s at position %d does not match the proposal", mid, i)
157+
}
158+
}
159+
return nil
160+
}
161+
116162
// verifyUpgradeProposal verifies the latest passed upgrade proposal to ensure the MREnclave hash matches.
117163
func (a *CountTXDecorator) verifyUpgradeProposal(ctx sdk.Context, msgUpgrade *types.MsgUpgradeProposalPassed) error {
118164
var proposals govtypes.Proposals

x/compute/internal/keeper/msg_server.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,41 @@ func (m msgServer) SetContractGovernance(goCtx context.Context, msg *types.MsgSe
313313

314314
return &types.MsgSetContractGovernanceResponse{}, nil
315315
}
316+
317+
func (m msgServer) UpdateMachineWhitelistProposal(goCtx context.Context, msg *types.MsgUpdateMachineWhitelistProposal) (*types.MsgUpdateMachineWhitelistProposalResponse, error) {
318+
ctx := sdk.UnwrapSDKContext(goCtx)
319+
320+
if err := msg.ValidateBasic(); err != nil {
321+
return nil, err
322+
}
323+
324+
// Verify sender has authority (only governance module)
325+
if m.keeper.authority != msg.Authority {
326+
return nil, errorsmod.Wrapf(govtypes.ErrInvalidSigner, "invalid authority; expected %s, got %s", m.keeper.authority, msg.Authority)
327+
}
328+
329+
ctx.EventManager().EmitEvent(sdk.NewEvent(
330+
types.EventTypeMachineWhitelistProposal,
331+
sdk.NewAttribute("machine_count", fmt.Sprintf("%d", len(msg.MachineIds))),
332+
sdk.NewAttribute(sdk.AttributeKeySender, msg.Authority),
333+
))
334+
335+
return &types.MsgUpdateMachineWhitelistProposalResponse{}, nil
336+
}
337+
338+
func (m msgServer) UpdateMachineWhitelist(goCtx context.Context, msg *types.MsgUpdateMachineWhitelist) (*types.MsgUpdateMachineWhitelistResponse, error) {
339+
ctx := sdk.UnwrapSDKContext(goCtx)
340+
341+
if err := msg.ValidateBasic(); err != nil {
342+
return nil, err
343+
}
344+
345+
ctx.EventManager().EmitEvent(sdk.NewEvent(
346+
types.EventTypeMachineWhitelistUpdate,
347+
sdk.NewAttribute("proposal_id", fmt.Sprintf("%d", msg.ProposalId)),
348+
sdk.NewAttribute("machine_count", fmt.Sprintf("%d", len(msg.MachineIds))),
349+
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender),
350+
))
351+
352+
return &types.MsgUpdateMachineWhitelistResponse{}, nil
353+
}

x/compute/internal/types/events.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const (
1717
EventTypeUpdateContractAdmin = "update_contract_admin"
1818
EventTypeUpgradeProposalPassed = "upgrade_proposal_passed"
1919
EventTypeContractGovernanceProposal = "contract_governance_proposal"
20+
EventTypeMachineWhitelistProposal = "machine_whitelist_proposal"
21+
EventTypeMachineWhitelistUpdate = "machine_whitelist_update"
2022
)
2123

2224
// event attributes returned from contract execution

x/compute/internal/types/msg.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,82 @@ func (msg MsgSetContractGovernance) GetSigners() []sdk.AccAddress {
318318
}
319319
return []sdk.AccAddress{senderAddr}
320320
}
321+
322+
func (msg MsgUpdateMachineWhitelistProposal) Route() string {
323+
return RouterKey
324+
}
325+
326+
func (msg MsgUpdateMachineWhitelistProposal) Type() string {
327+
return "update-machine-whitelist-proposal"
328+
}
329+
330+
func (msg MsgUpdateMachineWhitelistProposal) ValidateBasic() error {
331+
if _, err := sdk.AccAddressFromBech32(msg.Authority); err != nil {
332+
return errorsmod.Wrap(err, "invalid authority")
333+
}
334+
335+
if len(msg.MachineIds) == 0 {
336+
return errorsmod.Wrap(ErrEmpty, "machine_ids cannot be empty")
337+
}
338+
339+
for i, id := range msg.MachineIds {
340+
if len(id) != 20 {
341+
return errorsmod.Wrapf(ErrInvalid,
342+
"machine_id at index %d must be 20 bytes, got %d", i, len(id))
343+
}
344+
}
345+
346+
return nil
347+
}
348+
349+
func (msg MsgUpdateMachineWhitelistProposal) GetSignBytes() []byte {
350+
return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg))
351+
}
352+
353+
func (msg MsgUpdateMachineWhitelistProposal) GetSigners() []sdk.AccAddress {
354+
addr, err := sdk.AccAddressFromBech32(msg.Authority)
355+
if err != nil {
356+
panic(err.Error())
357+
}
358+
return []sdk.AccAddress{addr}
359+
}
360+
361+
func (msg MsgUpdateMachineWhitelist) Route() string {
362+
return RouterKey
363+
}
364+
365+
func (msg MsgUpdateMachineWhitelist) Type() string {
366+
return "update-machine-whitelist"
367+
}
368+
369+
func (msg MsgUpdateMachineWhitelist) ValidateBasic() error {
370+
if _, err := sdk.AccAddressFromBech32(msg.Sender); err != nil {
371+
return errorsmod.Wrap(err, "invalid sender address")
372+
}
373+
374+
if msg.ProposalId == 0 {
375+
return errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "proposal ID cannot be zero")
376+
}
377+
378+
if len(msg.MachineIds) == 0 {
379+
return errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "machine IDs cannot be empty")
380+
}
381+
382+
// Validate each machine ID is exactly 20 bytes
383+
for i, id := range msg.MachineIds {
384+
if len(id) != 20 {
385+
return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "machine ID at index %d must be 20 bytes, got %d", i, len(id))
386+
}
387+
}
388+
389+
return nil
390+
}
391+
392+
func (msg MsgUpdateMachineWhitelist) GetSignBytes() []byte {
393+
return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg))
394+
}
395+
396+
func (msg MsgUpdateMachineWhitelist) GetSigners() []sdk.AccAddress {
397+
addr, _ := sdk.AccAddressFromBech32(msg.Sender)
398+
return []sdk.AccAddress{addr}
399+
}

0 commit comments

Comments
 (0)