Skip to content

Commit d6e3b1d

Browse files
feat: proposal level decoding APIs (#309)
This pull request introduces new APIs for proposal-level decoding and includes several changes to the `proposal` and `timelock_proposal` modules to support these new features. The most important changes include adding the `DecodeProposal` method to both `Proposal` and `TimelockProposal` structs, updating tests to cover these new methods, and importing necessary packages. ### Proposal Decoding Enhancements: * Added `DecodeProposal` method to `Proposal` struct to decode raw transactions into human-readable operations. (`proposal.go`) * Added `DecodeProposal` method to `TimelockProposal` struct to decode raw transactions into human-readable operations. (`timelock_proposal.go`) ### Testing Enhancements: * Added `Test_Proposal_Decode` to test the `DecodeProposal` method for `Proposal` struct, including various success and failure scenarios. (`proposal_test.go`) * Added `Test_TimelockProposal_Decode` to test the `DecodeProposal` method for `TimelockProposal` struct, including various success and failure scenarios. (`timelock_proposal_test.go`) ### Dependency Updates: * Imported necessary packages for decoding and testing, including `crypto`, `mock`, and `bindings`. (`proposal_test.go`, `timelock_proposal_test.go`) [[1]](diffhunk://#diff-f54ef2790620db06a34dcf62d0565c5536ddeb0b22274fafc6d0cff158c9b789R8-R24) [[2]](diffhunk://#diff-b80686a8969c14a89af64e7ed45e5a1e0dc025880256405d9e48eac6e33722b9R8-R24) ### Documentation: * Added a changeset file to document the addition of proposal-level decoding related APIs. (`.changeset/fast-taxis-guess.md`) --------- Co-authored-by: Gustavo Gama <[email protected]>
1 parent 8b5981a commit d6e3b1d

5 files changed

+375
-0
lines changed

.changeset/fast-taxis-guess.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smartcontractkit/mcms": minor
3+
---
4+
5+
Add proposal-level decoding related APIs

proposal.go

+27
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,33 @@ func (p *Proposal) GetEncoders() (map[types.ChainSelector]sdk.Encoder, error) {
348348
return encoders, nil
349349
}
350350

351+
// Decode decodes the raw transactions into a list of human-readable operations.
352+
func (p *Proposal) Decode(decoders map[types.ChainSelector]sdk.Decoder, contractInterfaces map[string]string) ([]sdk.DecodedOperation, error) {
353+
decodedOps := make([]sdk.DecodedOperation, len(p.Operations))
354+
for i, op := range p.Operations {
355+
// Get the decoder for the chain selector
356+
decoder, ok := decoders[op.ChainSelector]
357+
if !ok {
358+
return nil, fmt.Errorf("no decoder found for chain selector %d", op.ChainSelector)
359+
}
360+
361+
// Get the contract interfaces for the contract type
362+
contractInterface, ok := contractInterfaces[op.Transaction.ContractType]
363+
if !ok {
364+
return nil, fmt.Errorf("no contract interfaces found for contract type %s", op.Transaction.ContractType)
365+
}
366+
367+
decodedOp, err := decoder.Decode(op.Transaction, contractInterface)
368+
if err != nil {
369+
return nil, fmt.Errorf("unable to decode operation: %w", err)
370+
}
371+
372+
decodedOps[i] = decodedOp
373+
}
374+
375+
return decodedOps, nil
376+
}
377+
351378
// proposalValidateBasic basic validation for an MCMS proposal
352379
func proposalValidateBasic(proposalObj Proposal) error {
353380
validUntil := time.Unix(int64(proposalObj.ValidUntil), 0)

proposal_test.go

+152
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,23 @@ import (
55
"errors"
66
"io"
77
"math"
8+
"math/big"
89
"os"
910
"strings"
1011
"testing"
1112

1213
"github.com/ethereum/go-ethereum/common"
14+
"github.com/ethereum/go-ethereum/crypto"
1315
"github.com/go-playground/validator/v10"
1416
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/mock"
1518
"github.com/stretchr/testify/require"
1619

1720
"github.com/smartcontractkit/mcms/internal/testutils/chaintest"
1821
"github.com/smartcontractkit/mcms/sdk"
1922
"github.com/smartcontractkit/mcms/sdk/evm"
23+
"github.com/smartcontractkit/mcms/sdk/evm/bindings"
24+
"github.com/smartcontractkit/mcms/sdk/mocks"
2025
"github.com/smartcontractkit/mcms/types"
2126
)
2227

@@ -804,6 +809,153 @@ func Test_Proposal_TransactionCounts(t *testing.T) {
804809
}, got)
805810
}
806811

812+
func Test_Proposal_Decode(t *testing.T) {
813+
t.Parallel()
814+
815+
// Get ABI
816+
timelockAbi, err := bindings.RBACTimelockMetaData.GetAbi()
817+
require.NoError(t, err)
818+
exampleRole := crypto.Keccak256Hash([]byte("EXAMPLE_ROLE"))
819+
820+
// Grant role data
821+
grantRoleData, err := timelockAbi.Pack("grantRole", [32]byte(exampleRole), common.HexToAddress("0x123"))
822+
require.NoError(t, err)
823+
824+
tests := []struct {
825+
name string
826+
setup func(t *testing.T) (map[types.ChainSelector]sdk.Decoder, map[string]string)
827+
give []types.Operation
828+
want []sdk.DecodedOperation
829+
wantErr string
830+
}{
831+
{
832+
name: "success: decodes an operation",
833+
setup: func(t *testing.T) (map[types.ChainSelector]sdk.Decoder, map[string]string) {
834+
t.Helper()
835+
decoders := map[types.ChainSelector]sdk.Decoder{
836+
chaintest.Chain1Selector: evm.NewDecoder(),
837+
}
838+
839+
return decoders, map[string]string{"RBACTimelock": bindings.RBACTimelockABI}
840+
},
841+
give: []types.Operation{
842+
{
843+
ChainSelector: chaintest.Chain1Selector,
844+
Transaction: evm.NewTransaction(
845+
common.HexToAddress("0xTestTarget"),
846+
grantRoleData,
847+
big.NewInt(0),
848+
"RBACTimelock",
849+
[]string{"grantRole"},
850+
),
851+
},
852+
},
853+
want: []sdk.DecodedOperation{
854+
&evm.DecodedOperation{
855+
FunctionName: "grantRole",
856+
InputKeys: []string{"role", "account"},
857+
InputArgs: []any{[32]byte(exampleRole.Bytes()), common.HexToAddress("0x0000000000000000000000000000000000000123")},
858+
},
859+
},
860+
wantErr: "",
861+
},
862+
{
863+
name: "failure: missing chain decoder",
864+
setup: func(t *testing.T) (map[types.ChainSelector]sdk.Decoder, map[string]string) {
865+
t.Helper()
866+
return map[types.ChainSelector]sdk.Decoder{}, map[string]string{}
867+
},
868+
give: []types.Operation{
869+
{
870+
ChainSelector: chaintest.Chain1Selector,
871+
Transaction: evm.NewTransaction(
872+
common.HexToAddress("0xTestTarget"),
873+
grantRoleData,
874+
big.NewInt(0),
875+
"RBACTimelock",
876+
[]string{"grantRole"},
877+
),
878+
},
879+
},
880+
want: nil,
881+
wantErr: "no decoder found for chain selector 3379446385462418246",
882+
},
883+
{
884+
name: "failure: missing contract ABI",
885+
setup: func(t *testing.T) (map[types.ChainSelector]sdk.Decoder, map[string]string) {
886+
t.Helper()
887+
decoders := map[types.ChainSelector]sdk.Decoder{
888+
chaintest.Chain1Selector: evm.NewDecoder(),
889+
}
890+
891+
return decoders, map[string]string{}
892+
},
893+
give: []types.Operation{
894+
{
895+
ChainSelector: chaintest.Chain1Selector,
896+
Transaction: evm.NewTransaction(
897+
common.HexToAddress("0xTestTarget"),
898+
grantRoleData,
899+
big.NewInt(0),
900+
"RBACTimelock",
901+
[]string{"grantRole"},
902+
),
903+
},
904+
},
905+
want: nil,
906+
wantErr: "no contract interfaces found for contract type RBACTimelock",
907+
},
908+
{
909+
name: "failure: unable to decode operation",
910+
setup: func(t *testing.T) (map[types.ChainSelector]sdk.Decoder, map[string]string) {
911+
t.Helper()
912+
mockDecoder := mocks.NewDecoder(t)
913+
mockDecoder.EXPECT().Decode(mock.Anything, mock.Anything).Return(nil, errors.New("decode error"))
914+
decoders := map[types.ChainSelector]sdk.Decoder{
915+
chaintest.Chain1Selector: mockDecoder,
916+
}
917+
918+
return decoders, map[string]string{"RBACTimelock": bindings.RBACTimelockABI}
919+
},
920+
give: []types.Operation{
921+
{
922+
ChainSelector: chaintest.Chain1Selector,
923+
Transaction: evm.NewTransaction(
924+
common.HexToAddress("0xTestTarget"),
925+
grantRoleData,
926+
big.NewInt(0),
927+
"RBACTimelock",
928+
[]string{"grantRole"},
929+
),
930+
},
931+
},
932+
want: nil,
933+
wantErr: "unable to decode operation: decode error",
934+
},
935+
}
936+
937+
for _, tt := range tests {
938+
t.Run(tt.name, func(t *testing.T) {
939+
t.Parallel()
940+
941+
proposal := Proposal{
942+
BaseProposal: BaseProposal{},
943+
Operations: tt.give,
944+
}
945+
946+
decoders, interfaces := tt.setup(t)
947+
got, err := proposal.Decode(decoders, interfaces)
948+
949+
if tt.wantErr != "" {
950+
require.EqualError(t, err, tt.wantErr)
951+
} else {
952+
require.NoError(t, err)
953+
assert.Equal(t, tt.want, got)
954+
}
955+
})
956+
}
957+
}
958+
807959
func Test_Proposal_TransactionNonces(t *testing.T) {
808960
t.Parallel()
809961

timelock_proposal.go

+29
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,35 @@ func (m *TimelockProposal) Convert(
186186
return result, predecessors, nil
187187
}
188188

189+
// Decode decodes the raw transactions into a list of human-readable operations.
190+
func (m *TimelockProposal) Decode(decoders map[types.ChainSelector]sdk.Decoder, contractInterfaces map[string]string) ([][]sdk.DecodedOperation, error) {
191+
decodedOps := make([][]sdk.DecodedOperation, len(m.Operations))
192+
for i, op := range m.Operations {
193+
// Get the decoder for the chain selector
194+
decoder, ok := decoders[op.ChainSelector]
195+
if !ok {
196+
return nil, fmt.Errorf("no decoder found for chain selector %d", op.ChainSelector)
197+
}
198+
199+
for _, tx := range op.Transactions {
200+
// Get the contract interfaces for the contract type
201+
contractInterface, ok := contractInterfaces[tx.ContractType]
202+
if !ok {
203+
return nil, fmt.Errorf("no contract interfaces found for contract type %s", tx.ContractType)
204+
}
205+
206+
decodedOp, err := decoder.Decode(tx, contractInterface)
207+
if err != nil {
208+
return nil, fmt.Errorf("unable to decode operation: %w", err)
209+
}
210+
211+
decodedOps[i] = append(decodedOps[i], decodedOp)
212+
}
213+
}
214+
215+
return decodedOps, nil
216+
}
217+
189218
// timeLockProposalValidateBasic basic validation for an MCMS proposal
190219
func timeLockProposalValidateBasic(timelockProposal TimelockProposal) error {
191220
// Get the current Unix timestamp as an int64

0 commit comments

Comments
 (0)