-
Notifications
You must be signed in to change notification settings - Fork 3
feat: ContractVersion in OperationMetadata (#979) #986
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
Changes from 9 commits
2aabdbf
5311b31
f7f09ea
42f1045
c314860
651039c
88be8e4
237b655
4d69cd7
d558e85
aad11d8
9620685
99a5e84
8bc04c0
5fa5480
dd9760c
c35fcdb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "chainlink-deployments-framework": minor | ||
| --- | ||
|
|
||
| Use Metadata.ContractVersion instead of ContractTypeAndVersion |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,13 @@ | ||
| package analyzer | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
|
|
||
| "github.com/smartcontractkit/mcms/sdk" | ||
| "github.com/smartcontractkit/mcms/sdk/ton" | ||
| "github.com/smartcontractkit/mcms/types" | ||
|
|
||
| "github.com/smartcontractkit/chainlink-deployments-framework/deployment" | ||
|
|
||
| "github.com/smartcontractkit/chainlink-ton/pkg/bindings" | ||
| ) | ||
|
|
||
|
|
@@ -37,29 +36,41 @@ func AnalyzeTONTransactions(ctx ProposalContext, chainSelector uint64, txs []typ | |
| // instead of returning an error. This allows the proposal to continue processing even if | ||
| // a single transaction fails to decode. | ||
| func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelector uint64, mcmsTx types.Transaction) (*DecodedCall, error) { | ||
| contractTypeAndVersion, err := deployment.TypeAndVersionFromString(mcmsTx.ContractTypeAndVersion) | ||
| if err != nil { | ||
| contractType, contractVersion := resolveContractInfo(ctx, chainSelector, mcmsTx) | ||
| errStr := fmt.Errorf("failed to decode TON transaction: failed to parse contract type and version: %w", err) | ||
| contractType, contractVersion := resolveContractInfo(ctx, chainSelector, mcmsTx) | ||
|
|
||
| return &DecodedCall{ | ||
| Address: mcmsTx.To, | ||
| Method: errStr.Error(), | ||
| ContractType: contractType, | ||
| ContractVersion: contractVersion, | ||
| }, nil | ||
| } | ||
| decodedOp, err := decoder.Decode(mcmsTx, contractTypeAndVersion.Type.String()) | ||
| var typeErr string | ||
| fullyQualifiedName := func() string { | ||
| var additionalFields ton.AdditionalFields | ||
| if err := json.Unmarshal(mcmsTx.AdditionalFields, &additionalFields); err != nil { | ||
| typeErr = fmt.Sprintf("failed to unmarshal TON additional fields: %s", err) | ||
| return "" | ||
|
patricios-space marked this conversation as resolved.
|
||
| } | ||
|
|
||
| fullyQualifiedName := string(additionalFields.ContractTypeFull) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Let's extract into a type/func which we document and allows anybody to produce a FQN |
||
| // If ContractVersion is provided, append it to the fully qualified name to ensure the decoder uses the correct version. | ||
| // If it is skipped, the decoder will use the latest version available for the contract type. | ||
| // Note: we don't use contractVersion from resolveContractInfo because that only represents the short type used by the datastore. | ||
|
patricios-space marked this conversation as resolved.
patricios-space marked this conversation as resolved.
|
||
| if mcmsTx.ContractVersion != nil { | ||
|
patricios-space marked this conversation as resolved.
|
||
| fullyQualifiedName += "@" + contractVersion | ||
| } | ||
|
Comment on lines
+45
to
+51
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't want this in TON because we only want to select a version explicitly via TX params |
||
|
|
||
| return fullyQualifiedName | ||
| }() | ||
|
|
||
| decodedOp, err := decoder.Decode(mcmsTx, fullyQualifiedName) | ||
| if err != nil { | ||
| // Don't return an error to not block the whole proposal decoding because of a single transaction decode failure. | ||
| // Instead, put the error message in the Method field so it's visible in the report. | ||
| errStr := fmt.Errorf("failed to decode TON transaction: %w", err) | ||
| errStr := "failed to decode TON transaction: " + err.Error() | ||
| if typeErr != "" { | ||
| errStr += "; additionally" + typeErr | ||
|
patricios-space marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| return &DecodedCall{ | ||
| Address: mcmsTx.To, | ||
| Method: errStr.Error(), | ||
| ContractType: contractTypeAndVersion.Type.String(), | ||
| ContractVersion: contractTypeAndVersion.Version.String(), | ||
| Method: errStr, | ||
| ContractType: contractType, | ||
| ContractVersion: contractVersion, | ||
| }, nil | ||
| } | ||
|
|
||
|
|
@@ -73,7 +84,7 @@ func AnalyzeTONTransaction(ctx ProposalContext, decoder sdk.Decoder, chainSelect | |
| Method: decodedOp.MethodName(), | ||
| Inputs: namedArgs, | ||
| Outputs: []NamedField{}, | ||
| ContractType: contractTypeAndVersion.Type.String(), | ||
| ContractVersion: contractTypeAndVersion.Version.String(), | ||
| ContractType: contractType, | ||
| ContractVersion: contractVersion, | ||
| }, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,12 +5,14 @@ import ( | |
| "math/big" | ||
| "testing" | ||
|
|
||
| "github.com/Masterminds/semver/v3" | ||
| "github.com/ethereum/go-ethereum/crypto" | ||
| "github.com/smartcontractkit/mcms/sdk/ton" | ||
| "github.com/smartcontractkit/mcms/types" | ||
| "github.com/stretchr/testify/require" | ||
| "github.com/xssnick/tonutils-go/address" | ||
| "github.com/xssnick/tonutils-go/tlb" | ||
| "github.com/xssnick/tonutils-go/tvm/cell" | ||
|
|
||
| "github.com/smartcontractkit/chainlink-ton/pkg/bindings" | ||
| "github.com/smartcontractkit/chainlink-ton/pkg/bindings/lib/access/rbac" | ||
|
|
@@ -49,25 +51,59 @@ func TestAnalyzeTONTransaction(t *testing.T) { | |
| { | ||
| name: "unknown contract type", | ||
| mcmsTx: types.Transaction{ | ||
| OperationMetadata: types.OperationMetadata{ContractType: "unknown.type", ContractTypeAndVersion: deployment.TypeAndVersion{Type: "com.example.package.unknown.contract"}.String()}, | ||
| OperationMetadata: types.OperationMetadata{ContractType: "unknown.type", ContractVersion: semver.MustParse("1.2.3")}, | ||
| To: testAddress, | ||
| Data: []byte{0x01, 0x02}, | ||
| AdditionalFields: json.RawMessage(`{"value":0}`), | ||
| Data: func() []byte { | ||
| c := cell.BeginCell() | ||
| c.MustStoreBinarySnake([]byte{0x01, 0x02}) | ||
|
|
||
| return c.EndCell().ToBOC() | ||
| }(), | ||
| AdditionalFields: json.RawMessage(`{"value":0, "contractTypeFull":"com.example.package.unknown.contract"}`), | ||
| }, | ||
| want: &DecodedCall{Address: testAddress}, | ||
| wantErrContain: "unknown contract interface: com.example.package.unknown.contract", | ||
| }, | ||
| { | ||
| name: "fail - unknown version", | ||
| mcmsTx: func() types.Transaction { | ||
| tx := setup.makeGrantRoleTx(t, 1) | ||
| tx.ContractVersion = semver.MustParse("0.0.0") | ||
|
|
||
| return tx | ||
| }(), | ||
| want: &DecodedCall{Address: setup.targetAddr.String()}, | ||
| wantErrContain: "unknown contract interface: link.chain.ton.lib.access.RBAC@0.0.0", | ||
| }, | ||
| { | ||
| name: "empty data", | ||
| mcmsTx: types.Transaction{ | ||
| OperationMetadata: types.OperationMetadata{ContractType: bindings.ShortRBAC, ContractTypeAndVersion: deployment.TypeAndVersion{Type: deployment.ContractType(bindings.TypeMCMS)}.String()}, | ||
| OperationMetadata: types.OperationMetadata{ContractType: bindings.ShortMCMS}, | ||
| To: testAddress, | ||
| Data: []byte{}, | ||
| AdditionalFields: json.RawMessage(`{"value":0}`), | ||
| AdditionalFields: json.RawMessage(`{"value":0, "contractTypeFull":"` + bindings.TypeMCMS + `"}`), | ||
| }, | ||
| want: &DecodedCall{Address: testAddress}, | ||
| wantErrContain: "invalid cell BOC data", | ||
| }, | ||
| { | ||
| name: "empty cell", | ||
| mcmsTx: types.Transaction{ | ||
| OperationMetadata: types.OperationMetadata{ContractType: bindings.ShortMCMS}, | ||
| To: testAddress, | ||
| Data: tvm.EmptyCell.ToBOC(), | ||
| AdditionalFields: json.RawMessage(`{"value":0}`), | ||
| }, | ||
| want: &DecodedCall{ | ||
| Address: testAddress, | ||
| Method: "::(0x0)", | ||
| Inputs: []NamedField{}, | ||
| Outputs: []NamedField{}, | ||
| ContractType: bindings.ShortMCMS, | ||
| ContractVersion: "", | ||
| }, | ||
| wantErrContain: "", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
|
|
@@ -173,7 +209,7 @@ func TestAnalyzeTONTransactions(t *testing.T) { | |
| continue | ||
| } | ||
|
|
||
| require.Equal(t, tt.want[i], result, "call %d", i) | ||
| require.Equal(t, tt.want[i], result) | ||
| } | ||
| }) | ||
| } | ||
|
|
@@ -187,7 +223,7 @@ func TestAnalyzeTONTransactionResolveContractInfo(t *testing.T) { | |
| chainSelector := uint64(123456) | ||
| tx := setup.makeGrantRoleTx(t, 1) | ||
|
|
||
| t.Run("ignores contract type and version from proposal context", func(t *testing.T) { | ||
| t.Run("resolves contract type and version from proposal context", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| ctx := &DefaultProposalContext{ | ||
|
|
@@ -200,8 +236,25 @@ func TestAnalyzeTONTransactionResolveContractInfo(t *testing.T) { | |
|
|
||
| result, err := AnalyzeTONTransaction(ctx, decoder, chainSelector, tx) | ||
| require.NoError(t, err) | ||
| require.EqualValues(t, bindings.TypeRBAC, result.ContractType) | ||
| require.Equal(t, "0.0.0", result.ContractVersion) | ||
| require.Equal(t, "TONRBAC", result.ContractType) | ||
| require.Equal(t, "1.2.3", result.ContractVersion) | ||
| }) | ||
|
|
||
| t.Run("falls back when address is not in proposal context", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| ctx := &DefaultProposalContext{ | ||
| AddressesByChain: deployment.AddressesByChain{ | ||
| chainSelector: { | ||
| "EQCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": deployment.MustTypeAndVersionFromString("OtherContract 9.9.9"), | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| result, err := AnalyzeTONTransaction(ctx, decoder, chainSelector, tx) | ||
| require.NoError(t, err) | ||
| require.Equal(t, tx.ContractType, result.ContractType) | ||
| require.Empty(t, result.ContractVersion) | ||
| }) | ||
| } | ||
|
|
||
|
|
@@ -232,7 +285,8 @@ func (s *testTONSetup) makeGrantRoleTx(t *testing.T, queryID uint64) types.Trans | |
| grantRoleData.ToBuilder().ToSlice(), | ||
| big.NewInt(0), | ||
| bindings.ShortRBAC, | ||
| deployment.TypeAndVersion{Type: deployment.ContractType(bindings.TypeRBAC)}.String(), | ||
| nil, | ||
| bindings.TypeRBAC, | ||
| []string{"grantRole"}, | ||
| ) | ||
| require.NoError(t, err) | ||
|
|
@@ -242,10 +296,9 @@ func (s *testTONSetup) makeGrantRoleTx(t *testing.T, queryID uint64) types.Trans | |
|
|
||
| func (s *testTONSetup) expectedGrantRoleCall(queryID uint64) *DecodedCall { | ||
| return &DecodedCall{ | ||
| Address: s.targetAddr.String(), | ||
| Method: string(bindings.TypeRBAC) + "::GrantRole(0x95cd540f)", | ||
| ContractType: string(bindings.TypeRBAC), | ||
| ContractVersion: "0.0.0", | ||
| Address: s.targetAddr.String(), | ||
| Method: string(bindings.TypeRBAC) + "::GrantRole(0x95cd540f)", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this DecodedCall should also contain a contract version, maybe as part of the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was already done but it was not covered by test. Added a case to confirm this |
||
| ContractType: string(bindings.ShortRBAC), | ||
| Inputs: []NamedField{ | ||
| {Name: "QueryID", Value: SimpleField{Value: bigIntStr(queryID)}, RawValue: queryID}, | ||
| {Name: "Role", Value: SimpleField{Value: s.exampleRoleBig.String()}, RawValue: tlbe.NewUint256(s.exampleRoleBig)}, | ||
|
|
@@ -259,11 +312,11 @@ func bigIntStr(v uint64) string { | |
| return new(big.Int).SetUint64(v).String() | ||
| } | ||
|
|
||
| func makeInvalidTx(contractType string, contractFQN tvm.FullyQualifiedName) types.Transaction { | ||
| func makeInvalidTx(contractType string, contractTypeFull tvm.FullyQualifiedName) types.Transaction { | ||
| return types.Transaction{ | ||
| OperationMetadata: types.OperationMetadata{ContractType: contractType, ContractTypeAndVersion: deployment.TypeAndVersion{Type: deployment.ContractType(contractFQN)}.String()}, | ||
| OperationMetadata: types.OperationMetadata{ContractType: contractType}, | ||
| To: testAddress, | ||
| Data: []byte{0xFF, 0xFF}, | ||
| AdditionalFields: json.RawMessage(`{"value":0}`), | ||
| AdditionalFields: json.RawMessage(`{"value":0, "contractTypeFull":"` + contractTypeFull + `"}`), | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be the opposite: the datastore value should have precedence over the proposal metadata.
@ecPablo @ChrisAmora , what are your thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's more likely that datastore has more up-to-date data. For example, I generate proposalA that uses contractVersion 1.0.0, then very shortly after, someone updates datastore to contractVersion 2.0.0. I'd expect the decoded version to show me the 2.0.0 as that's the current on chain state. The only reasons where this could not be true is if we have out-of-cld systems updating the contracts, in that case maybe the proposal might be safer source of truth. But I don't think that's the case here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, let's keep system context (DS) to have priority. Override can be made with updating the DS directly with the proposal PR (if something is off).
Finally, we should also be able to interact with ad-hoc contracts which are not stored, and for that cases the proposal input ref (type, version) should resolve properly.