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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
}

// 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
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