Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: construct queued proposals with predecessors #276

Merged
merged 16 commits into from
Feb 3, 2025
5 changes: 5 additions & 0 deletions .changeset/shaggy-pianos-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smartcontractkit/mcms": minor
---

Update constructors to add predecessor proposals for queuing
3 changes: 2 additions & 1 deletion e2e/ledger/ledger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package ledger

import (
"context"
"io"
"log"
"math/big"
"os"
Expand Down Expand Up @@ -203,7 +204,7 @@ func (s *ManualLedgerSigningTestSuite) TestManualLedgerSigning() {
}(file)
s.Require().NoError(err)

proposal, err := mcms.NewProposal(file)
proposal, err := mcms.NewProposal(file, []io.Reader{})
s.Require().NoError(err, "Failed to parse proposal")
s.T().Log("Proposal loaded successfully.")
proposal.ChainMetadata[s.chainSelectorEVM] = types.ChainMetadata{
Expand Down
4 changes: 2 additions & 2 deletions e2e/tests/evm/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (s *SigningTestSuite) TestReadAndSign() {
}
}(file)
s.Require().NoError(err)
proposal, err := mcms.NewProposal(file)
proposal, err := mcms.NewProposal(file, []io.Reader{})
s.Require().NoError(err)
s.Require().NotNil(proposal)
inspectors := map[mcmtypes.ChainSelector]sdk.Inspector{
Expand Down Expand Up @@ -81,7 +81,7 @@ func (s *SigningTestSuite) TestReadAndSign() {
_, err = tmpFile.Seek(0, io.SeekStart)
s.Require().NoError(err, "Failed to reset file pointer to the start")

writtenProposal, err := mcms.NewProposal(tmpFile)
writtenProposal, err := mcms.NewProposal(tmpFile, []io.Reader{})
s.Require().NoError(err)

// Validate the appended signature
Expand Down
43 changes: 29 additions & 14 deletions proposal.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const SignMsgABI = `[{"type":"bytes32"},{"type":"uint32"}]`

type ProposalInterface interface {
AppendSignature(signature types.Signature)
TransactionCounts() map[types.ChainSelector]uint64
ChainMetadatas() map[types.ChainSelector]types.ChainMetadata
SetChainMetadata(chainSelector types.ChainSelector, metadata types.ChainMetadata)
Validate() error
}

Expand All @@ -41,7 +44,7 @@ func LoadProposal(proposalType types.ProposalKind, filePath string) (ProposalInt
// Ensure the file is closed when done
defer file.Close()

return NewProposal(file)
return NewProposal(file, []io.Reader{}) // TODO: inject predecessors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to ask: is this a blocker?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh shoot not a blocker but that was a miss

case types.KindTimelockProposal:
// Open the file
file, err := os.Open(filePath)
Expand All @@ -52,7 +55,7 @@ func LoadProposal(proposalType types.ProposalKind, filePath string) (ProposalInt
// Ensure the file is closed when done
defer file.Close()

return NewTimelockProposal(file)
return NewTimelockProposal(file, []io.Reader{}) // TODO: inject predecessors
default:
return nil, errors.New("unknown proposal type")
}
Expand All @@ -78,6 +81,16 @@ func (p *BaseProposal) AppendSignature(signature types.Signature) {
p.Signatures = append(p.Signatures, signature)
}

// ChainMetadata returns the chain metadata for the proposal.
func (p *BaseProposal) ChainMetadatas() map[types.ChainSelector]types.ChainMetadata {
return p.ChainMetadata
akhilchainani marked this conversation as resolved.
Show resolved Hide resolved
}

// SetChainMetadata sets the chain metadata for a given chain selector.
func (p *BaseProposal) SetChainMetadata(chainSelector types.ChainSelector, metadata types.ChainMetadata) {
p.ChainMetadata[chainSelector] = metadata
}

// Proposal is a struct where the target contract is an MCMS contract
// with no forwarder contracts. This type does not support any type of atomic contract
// call batching, as the MCMS contract natively doesn't support batching
Expand All @@ -87,18 +100,20 @@ type Proposal struct {
Operations []types.Operation `json:"operations" validate:"required,min=1,dive"`
}

// NewProposal unmarshal data from the reader to JSON and returns a new Proposal.
func NewProposal(reader io.Reader) (*Proposal, error) {
var p Proposal
if err := json.NewDecoder(reader).Decode(&p); err != nil {
return nil, err
}

if err := p.Validate(); err != nil {
return nil, err
}

return &p, nil
var _ ProposalInterface = (*Proposal)(nil)

// NewProposal unmarshals data from the reader to JSON and returns a new Proposal.
// The predecessors parameter is a list of readers that contain the predecessors
// for the proposal for configuring operations counts, which makes the following
// assumptions:
// - The order of the predecessors array is the order in which the proposals are
// intended to be executed.
// - The op counts for the first proposal are meant to be the starting op for the
// full set of proposals.
// - The op counts for all other proposals except the first are ignored
// - all proposals are configured correctly and need no additional modifications
func NewProposal(reader io.Reader, predecessors []io.Reader) (*Proposal, error) {
return newProposal[*Proposal](reader, predecessors)
}

// WriteProposal marshals the proposal to JSON and writes it to the provided writer.
Expand Down
125 changes: 113 additions & 12 deletions proposal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ var (
"kind": "Proposal",
"validUntil": 2004259681,
"chainMetadata": {
"3379446385462418246": {}
"3379446385462418246": {
"startingOpCount": 0,
"mcmAddress": ""
}
},
"operations": [
{
Expand Down Expand Up @@ -56,18 +59,54 @@ func Test_BaseProposal_AppendSignature(t *testing.T) {
assert.Equal(t, []types.Signature{signature}, proposal.Signatures)
}

func Test_BaseProposal_GetChainMetadata(t *testing.T) {
t.Parallel()

chainMetadata := map[types.ChainSelector]types.ChainMetadata{
chaintest.Chain1Selector: {},
}

proposal := BaseProposal{
ChainMetadata: chainMetadata,
}

assert.Equal(t, chainMetadata, proposal.ChainMetadatas())
}

func Test_BaseProposal_SetChainMetadata(t *testing.T) {
t.Parallel()

proposal := BaseProposal{
ChainMetadata: map[types.ChainSelector]types.ChainMetadata{},
}

md, ok := proposal.ChainMetadata[chaintest.Chain1Selector]
assert.False(t, ok)
assert.Empty(t, md)

proposal.SetChainMetadata(chaintest.Chain1Selector, types.ChainMetadata{
StartingOpCount: 0,
MCMAddress: "",
})

assert.Equal(t, uint64(0), proposal.ChainMetadata[chaintest.Chain1Selector].StartingOpCount)
assert.Equal(t, "", proposal.ChainMetadata[chaintest.Chain1Selector].MCMAddress)
}

func Test_NewProposal(t *testing.T) {
t.Parallel()

tests := []struct {
name string
give string
want Proposal
wantErr string
name string
give string
givePredecessors []string
want Proposal
wantErr string
}{
{
name: "success: initializes a proposal from an io.Reader",
give: ValidProposal,
name: "success: initializes a proposal from an io.Reader",
give: ValidProposal,
givePredecessors: []string{},
want: Proposal{
BaseProposal: BaseProposal{
Version: "v1",
Expand All @@ -90,9 +129,66 @@ func Test_NewProposal(t *testing.T) {
},
},
{
name: "failure: could not unmarshal JSON",
give: `invalid`,
wantErr: "invalid character 'i' looking for beginning of value",
name: "success: initializes a proposal with 1 predecessor proposals",
give: ValidProposal,
givePredecessors: []string{ValidProposal},
want: Proposal{
BaseProposal: BaseProposal{
Version: "v1",
Kind: types.KindProposal,
ValidUntil: 2004259681,
ChainMetadata: map[types.ChainSelector]types.ChainMetadata{
chaintest.Chain1Selector: {
StartingOpCount: 1,
MCMAddress: "",
},
},
},
Operations: []types.Operation{
{
ChainSelector: chaintest.Chain1Selector,
Transaction: types.Transaction{
To: "0xsomeaddress",
Data: []byte{0x12, 0x33}, // Representing "0x123" as bytes
gustavogama-cll marked this conversation as resolved.
Show resolved Hide resolved
AdditionalFields: json.RawMessage(`{"value": 0}`), // JSON-encoded `{"value": 0}`
},
},
},
},
},
{
name: "success: initializes a proposal with 2 predecessor proposals",
give: ValidProposal,
givePredecessors: []string{ValidProposal, ValidProposal},
want: Proposal{
BaseProposal: BaseProposal{
Version: "v1",
Kind: types.KindProposal,
ValidUntil: 2004259681,
ChainMetadata: map[types.ChainSelector]types.ChainMetadata{
chaintest.Chain1Selector: {
StartingOpCount: 2,
MCMAddress: "",
},
},
},
Operations: []types.Operation{
{
ChainSelector: chaintest.Chain1Selector,
Transaction: types.Transaction{
To: "0xsomeaddress",
Data: []byte{0x12, 0x33}, // Representing "0x123" as bytes
AdditionalFields: json.RawMessage(`{"value": 0}`), // JSON-encoded `{"value": 0}`
},
},
},
},
},
{
name: "failure: could not unmarshal JSON",
give: `invalid`,
givePredecessors: []string{},
wantErr: "invalid character 'i' looking for beginning of value",
},
{
name: "failure: invalid proposal",
Expand All @@ -103,7 +199,8 @@ func Test_NewProposal(t *testing.T) {
"chainMetadata": {},
"operations": []
}`,
wantErr: "Key: 'Proposal.BaseProposal.ChainMetadata' Error:Field validation for 'ChainMetadata' failed on the 'min' tag\nKey: 'Proposal.Operations' Error:Field validation for 'Operations' failed on the 'min' tag",
givePredecessors: []string{},
wantErr: "Key: 'Proposal.BaseProposal.ChainMetadata' Error:Field validation for 'ChainMetadata' failed on the 'min' tag\nKey: 'Proposal.Operations' Error:Field validation for 'Operations' failed on the 'min' tag",
},
}

Expand All @@ -112,8 +209,12 @@ func Test_NewProposal(t *testing.T) {
t.Parallel()

give := strings.NewReader(tt.give)
givePredecessors := []io.Reader{}
for _, p := range tt.givePredecessors {
givePredecessors = append(givePredecessors, strings.NewReader(p))
}

fileProposal, err := NewProposal(give)
fileProposal, err := NewProposal(give, givePredecessors)

if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
Expand Down
34 changes: 23 additions & 11 deletions timelock_proposal.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,20 @@ type TimelockProposal struct {
SaltOverride *common.Hash `json:"salt,omitempty"`
}

// NewTimelockProposal unmarshal data from the reader to JSON and returns a new TimelockProposal.
func NewTimelockProposal(r io.Reader) (*TimelockProposal, error) {
var p TimelockProposal
if err := json.NewDecoder(r).Decode(&p); err != nil {
return nil, err
}
var _ ProposalInterface = (*TimelockProposal)(nil)

if err := p.Validate(); err != nil {
return nil, err
}

return &p, nil
// NewTimelockProposal unmarshal data from the reader to JSON and returns a new TimelockProposal.
// The predecessors parameter is a list of readers that contain the predecessors
// for the proposal for configuring operations counts, which makes the following
// assumptions:
// - The order of the predecessors array is the order in which the proposals are
// intended to be executed.
// - The op counts for the first proposal are meant to be the starting op for the
// full set of proposals.
// - The op counts for all other proposals except the first are ignored
// - all proposals are configured correctly and need no additional modifications
func NewTimelockProposal(r io.Reader, predecessors []io.Reader) (*TimelockProposal, error) {
return newProposal[*TimelockProposal](r, predecessors)
}

func WriteTimelockProposal(w io.Writer, p *TimelockProposal) error {
Expand All @@ -49,6 +51,16 @@ func WriteTimelockProposal(w io.Writer, p *TimelockProposal) error {
return enc.Encode(p)
}

// TransactionCounts returns the number of transactions for each chain in the proposal
func (m *TimelockProposal) TransactionCounts() map[types.ChainSelector]uint64 {
counts := make(map[types.ChainSelector]uint64)
for _, op := range m.Operations {
counts[op.ChainSelector] += uint64(len(op.Transactions))
}

return counts
}

// Salt returns a unique salt for the proposal.
// We need the salt to be unique in case you use an identical operation again
// on the same chain across two different proposals. Predecessor protects against
Expand Down
Loading
Loading