Repository: ovrclk/akash
Pull request #748 (2020-07-14 23:07:15)

feat:565 Deployment Version Field
Implementation of passing a Deployment manifest's Version from Create and Update commands and messages.

* `sdl.VersionSha` package produces deterministic version hash from SDL struct.
* `akashctl tx deployment [create|update]` generates and sets Version field.
* `testutil` package updated to provide a consistent Deployment.Version value.
  * `testutil.DeploymentVersion()` provides random version sums.
* Assertions/test that the Version is passed to the keeper via
`MsgUpdateDeployment` & `MsgCreateDeployment`.
  * MsgUpdateDeployment passes GroupSpec to update image tags
* Test integration tests for `deployment update`.
  * New deployment-v2.yaml with tag change.
  * Assert's Version field is changed post update.
* Tests to validate different deployment Versions
  * Compare differing deployment yaml sha256 sums in tests.
* Deployment* SDKEvent types carry Version field.
  * `EventDeploymentCreated` & `EventDeploymentUpdated`
  * Structs and constructors updated with Version field.
  * Tests updated to create and pass Versions for Create & Update.
* Test Simulations execute Update Deployments with different manifest
file.

fixes: #565


codecov[bot]: # [Codecov](https://codecov.io/gh/ovrclk/akash/pull/748?src=pr&el=h1) Report
> Merging [#748](https://codecov.io/gh/ovrclk/akash/pull/748?src=pr&el=desc) into [master](https://codecov.io/gh/ovrclk/akash/commit/9ecdb30e70d8b05333e2841be0949092f0256c44&el=desc) will **increase** coverage by `0.03%`.
> The diff coverage is `34.09%`.

[![Impacted file tree graph](https://codecov.io/gh/ovrclk/akash/pull/748/graphs/tree.svg?width=650&height=150&src=pr&token=QXe3zmrbI2)](https://codecov.io/gh/ovrclk/akash/pull/748?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #748      +/-   ##
==========================================
+ Coverage   31.63%   31.67%   +0.03%     
==========================================
  Files         151      151              
  Lines        7649     7720      +71     
==========================================
+ Hits         2420     2445      +25     
- Misses       5108     5151      +43     
- Partials      121      124       +3     
```


| [Impacted Files](https://codecov.io/gh/ovrclk/akash/pull/748?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [sdl/v1.go](https://codecov.io/gh/ovrclk/akash/pull/748/diff?src=pr&el=tree#diff-c2RsL3YxLmdv) | `76.56% <ø> (ø)` | |
| [x/deployment/client/cli/tx.go](https://codecov.io/gh/ovrclk/akash/pull/748/diff?src=pr&el=tree#diff-eC9kZXBsb3ltZW50L2NsaWVudC9jbGkvdHguZ28=) | `0.00% <0.00%> (ø)` | |
| [x/deployment/client/cli/util.go](https://codecov.io/gh/ovrclk/akash/pull/748/diff?src=pr&el=tree#diff-eC9kZXBsb3ltZW50L2NsaWVudC9jbGkvdXRpbC5nbw==) | `0.00% <ø> (ø)` | |
| [x/deployment/simulation/operations.go](https://codecov.io/gh/ovrclk/akash/pull/748/diff?src=pr&el=tree#diff-eC9kZXBsb3ltZW50L3NpbXVsYXRpb24vb3BlcmF0aW9ucy5nbw==) | `0.00% <0.00%> (ø)` | |
| [x/deployment/types/msgs.go](https://codecov.io/gh/ovrclk/akash/pull/748/diff?src=pr&el=tree#diff-eC9kZXBsb3ltZW50L3R5cGVzL21zZ3MuZ28=) | `0.00% <0.00%> (ø)` | |
| [x/deployment/types/event.go](https://codecov.io/gh/ovrclk/akash/pull/748/diff?src=pr&el=tree#diff-eC9kZXBsb3ltZW50L3R5cGVzL2V2ZW50Lmdv) | `93.79% <71.42%> (-4.45%)` | :arrow_down: |
| [sdl/sdl.go](https://codecov.io/gh/ovrclk/akash/pull/748/diff?src=pr&el=tree#diff-c2RsL3NkbC5nbw==) | `50.00% <75.00%> (ø)` | |
| [testutil/ids.go](https://codecov.io/gh/ovrclk/akash/pull/748/diff?src=pr&el=tree#diff-dGVzdHV0aWwvaWRzLmdv) | `84.37% <75.00%> (-3.13%)` | :arrow_down: |
| [testutil/deployment.go](https://codecov.io/gh/ovrclk/akash/pull/748/diff?src=pr&el=tree#diff-dGVzdHV0aWwvZGVwbG95bWVudC5nbw==) | `100.00% <100.00%> (ø)` | |
| [x/deployment/handler/handler.go](https://codecov.io/gh/ovrclk/akash/pull/748/diff?src=pr&el=tree#diff-eC9kZXBsb3ltZW50L2hhbmRsZXIvaGFuZGxlci5nbw==) | `70.27% <100.00%> (+1.25%)` | :arrow_up: |
| ... and [3 more](https://codecov.io/gh/ovrclk/akash/pull/748/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/ovrclk/akash/pull/748?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/ovrclk/akash/pull/748?src=pr&el=footer). Last update [9ecdb30...6d93acc](https://codecov.io/gh/ovrclk/akash/pull/748?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/sdl/sdl.go b/sdl/sdl.go
index bbc19665..0ee52a45 100644
--- a/sdl/sdl.go
+++ b/sdl/sdl.go
@@ -1,12 +1,17 @@
 package sdl
 
 import (
+	"crypto/sha256"
+	"encoding/json"
 	"io/ioutil"
 
+	yaml "gopkg.in/yaml.v2"
+
+	sdk "github.com/cosmos/cosmos-sdk/types"
+
 	"github.com/ovrclk/akash/manifest"
 	"github.com/ovrclk/akash/validation"
 	dtypes "github.com/ovrclk/akash/x/deployment/types"
-	yaml "gopkg.in/yaml.v2"
 )
 
 // SDL is the interface which wraps Validate, Deployment and Manifest methods
@@ -27,7 +32,7 @@ func ReadFile(path string) (SDL, error) {
 
 // Read reads buffer data and returns SDL instance
 func Read(buf []byte) (SDL, error) {
-	// TODO: handle versions
+	// TODO: handle versions; read 'version' field and switch
 	obj := &v1{}
 	if err := yaml.Unmarshal(buf, obj); err != nil {
 		return nil, err
@@ -55,6 +60,8 @@ func Read(buf []byte) (SDL, error) {
 	if err != nil {
 		return nil, err
 	}
+
+	// TODO: Determine if worth repairing ValidateManifest; functionality is commented out
 	if err := validation.ValidateManifest(m); err != nil {
 		return nil, err
 	}
@@ -64,3 +71,25 @@ func Read(buf []byte) (SDL, error) {
 
 	return obj, nil
 }
+
+// Version creates the deterministic Deployment Version hash from the SDL.
+// Sha256 returns 32 byte  sum of the SDL.
+func Version(s SDL) ([]byte, error) {
+	manifest, err := s.Manifest()
+	if err != nil {
+		return nil, err
+	}
+
+	m, err := json.Marshal(manifest)
+	if err != nil {
+		return nil, err
+	}
+
+	sortedBytes, err := sdk.SortJSON(m)
+	if err != nil {
+		return nil, err
+	}
+
+	sum := sha256.Sum256(sortedBytes)
+	return []byte(sum[:]), nil
+}
diff --git a/sdl/v1.go b/sdl/v1.go
index 4ec1463a..8cd1320c 100644
--- a/sdl/v1.go
+++ b/sdl/v1.go
@@ -17,8 +17,10 @@ var (
 	allowedVersion = semver.MustParse("1.5.0")
 )
 
+var _ SDL = (*v1)(nil) // Assert interface implementation
+
 type v1 struct {
-	Version  string
+	Version  string   `yaml:"version"`
 	Include  []string `yaml:",omitempty"`
 	Services map[string]v1Service
 	Profiles v1Profiles
diff --git a/x/deployment/client/cli/tx.go b/x/deployment/client/cli/tx.go
index 2def3c50..e576f8d1 100644
--- a/x/deployment/client/cli/tx.go
+++ b/x/deployment/client/cli/tx.go
@@ -45,12 +45,12 @@ func cmdCreate(key string, cdc *codec.Codec) *cobra.Command {
 			ctx := context.NewCLIContext().WithCodec(cdc)
 			bldr := auth.NewTxBuilderFromCLI(os.Stdin).WithTxEncoder(utils.GetTxEncoder(cdc))
 
-			sdl, err := sdl.ReadFile(args[0])
+			sdlManifest, err := sdl.ReadFile(args[0])
 			if err != nil {
 				return err
 			}
 
-			groups, err := sdl.DeploymentGroups()
+			groups, err := sdlManifest.DeploymentGroups()
 			if err != nil {
 				return err
 			}
@@ -67,10 +67,15 @@ func cmdCreate(key string, cdc *codec.Codec) *cobra.Command {
 				}
 			}
 
+			version, err := sdl.Version(sdlManifest)
+			if err != nil {
+				return err
+			}
+
 			msg := types.MsgCreateDeployment{
-				ID: id,
-				// Version:  []byte{0x1, 0x2},
-				Groups: make([]types.GroupSpec, 0, len(groups)),
+				ID:      id,
+				Version: version,
+				Groups:  make([]types.GroupSpec, 0, len(groups)),
 			}
 
 			for _, group := range groups {
@@ -126,8 +131,28 @@ func cmdUpdate(key string, cdc *codec.Codec) *cobra.Command {
 				return err
 			}
 
+			sdlManifest, err := sdl.ReadFile(args[0])
+			if err != nil {
+				return err
+			}
+			groups, err := sdlManifest.DeploymentGroups()
+			if err != nil {
+				return err
+			}
+
+			version, err := sdl.Version(sdlManifest)
+			if err != nil {
+				return err
+			}
+
 			msg := types.MsgUpdateDeployment{
-				ID: id,
+				ID:      id,
+				Version: version,
+				Groups:  make([]types.GroupSpec, 0, len(groups)),
+			}
+
+			for _, group := range groups {
+				msg.Groups = append(msg.Groups, *group)
 			}
 
 			return utils.GenerateOrBroadcastMsgs(ctx, bldr, []sdk.Msg{msg})
diff --git a/x/deployment/client/cli/util.go b/x/deployment/client/cli/util.go
index 48745cae..df801338 100644
--- a/x/deployment/client/cli/util.go
+++ b/x/deployment/client/cli/util.go
@@ -1,6 +1,8 @@
 package cli
 
-import "github.com/cosmos/cosmos-sdk/client/context"
+import (
+	"github.com/cosmos/cosmos-sdk/client/context"
+)
 
 func currentBlockHeight(ctx context.CLIContext) (uint64, error) {
 	client, err := ctx.GetNode()
diff --git a/x/deployment/handler/handler.go b/x/deployment/handler/handler.go
index 63a4300b..10c70181 100644
--- a/x/deployment/handler/handler.go
+++ b/x/deployment/handler/handler.go
@@ -1,6 +1,8 @@
 package handler
 
 import (
+	"bytes"
+
 	sdk "github.com/cosmos/cosmos-sdk/types"
 	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 	"github.com/pkg/errors"
@@ -36,8 +38,7 @@ func handleMsgCreate(ctx sdk.Context, keeper keeper.Keeper, _ MarketKeeper, msg
 	deployment := types.Deployment{
 		DeploymentID: msg.ID,
 		State:        types.DeploymentActive,
-		// TODO: version
-		// Version: sdk.Address.Bytes(),
+		Version:      msg.Version,
 	}
 
 	if err := validation.ValidateDeploymentGroups(msg.Groups); err != nil {
@@ -69,8 +70,9 @@ func handleMsgUpdate(ctx sdk.Context, keeper keeper.Keeper, _ MarketKeeper, msg
 		return nil, types.ErrDeploymentNotFound
 	}
 
-	// TODO: version
-	// deployment.Version = msg.Version
+	if bytes.Compare(msg.Version, deployment.Version) != 0 {
+		deployment.Version = msg.Version
+	}
 
 	if err := keeper.UpdateDeployment(ctx, deployment); err != nil {
 		return nil, errors.Wrap(types.ErrInternal, err.Error())
diff --git a/x/deployment/keeper/keeper.go b/x/deployment/keeper/keeper.go
index dd6c3c1d..6804992f 100644
--- a/x/deployment/keeper/keeper.go
+++ b/x/deployment/keeper/keeper.go
@@ -126,7 +126,7 @@ func (k Keeper) Create(ctx sdk.Context, deployment types.Deployment, groups []ty
 	}
 
 	ctx.EventManager().EmitEvent(
-		types.NewEventDeploymentCreated(deployment.ID()).
+		types.NewEventDeploymentCreated(deployment.ID(), deployment.Version).
 			ToSDKEvent(),
 	)
 
@@ -143,7 +143,7 @@ func (k Keeper) UpdateDeployment(ctx sdk.Context, deployment types.Deployment) e
 	}
 
 	ctx.EventManager().EmitEvent(
-		types.NewEventDeploymentUpdated(deployment.ID()).
+		types.NewEventDeploymentUpdated(deployment.ID(), deployment.Version).
 			ToSDKEvent(),
 	)
 
diff --git a/x/deployment/simulation/operations.go b/x/deployment/simulation/operations.go
index 47e107c7..d6a52c75 100644
--- a/x/deployment/simulation/operations.go
+++ b/x/deployment/simulation/operations.go
@@ -13,7 +13,7 @@ import (
 	"github.com/cosmos/cosmos-sdk/x/simulation"
 
 	simappparams "github.com/ovrclk/akash/app/params"
-	"github.com/ovrclk/akash/sdl"
+	sdlv1 "github.com/ovrclk/akash/sdl"
 	"github.com/ovrclk/akash/x/deployment/keeper"
 	"github.com/ovrclk/akash/x/deployment/types"
 )
@@ -98,7 +98,7 @@ func SimulateMsgCreateDeployment(ak govtypes.AccountKeeper, k keeper.Keeper) sim
 			return simulation.NoOpMsg(types.ModuleName), nil, nil
 		}
 
-		sdl, readError := sdl.ReadFile("../x/deployment/testdata/deployment.yaml")
+		sdl, readError := sdlv1.ReadFile("../x/deployment/testdata/deployment.yaml")
 		if readError != nil {
 			return simulation.NoOpMsg(types.ModuleName), nil, readError
 		}
@@ -107,6 +107,10 @@ func SimulateMsgCreateDeployment(ak govtypes.AccountKeeper, k keeper.Keeper) sim
 		if groupErr != nil {
 			return simulation.NoOpMsg(types.ModuleName), nil, groupErr
 		}
+		sdlSum, err := sdlv1.Version(sdl)
+		if err != nil {
+			return simulation.NoOpMsg(types.ModuleName), nil, errors.Wrap(err, "error parsing deployment version sum")
+		}
 
 		account := ak.GetAccount(ctx, simAccount.Address)
 
@@ -116,8 +120,9 @@ func SimulateMsgCreateDeployment(ak govtypes.AccountKeeper, k keeper.Keeper) sim
 		}
 
 		msg := types.MsgCreateDeployment{
-			ID:     dID,
-			Groups: make([]types.GroupSpec, 0, len(groupSpecs)),
+			ID:      dID,
+			Groups:  make([]types.GroupSpec, 0, len(groupSpecs)),
+			Version: sdlSum,
 		}
 
 		for _, spec := range groupSpecs {
@@ -168,6 +173,21 @@ func SimulateMsgUpdateDeployment(ak govtypes.AccountKeeper, k keeper.Keeper) sim
 			return simulation.NoOpMsg(types.ModuleName), nil, errors.Errorf("deployment with %s not found", deployment.ID().Owner)
 		}
 
+		sdl, readError := sdlv1.ReadFile("../x/deployment/testdata/deployment-v2.yaml")
+		if readError != nil {
+			return simulation.NoOpMsg(types.ModuleName), nil, readError
+		}
+
+		groupSpecs, groupErr := sdl.DeploymentGroups()
+		if groupErr != nil {
+			return simulation.NoOpMsg(types.ModuleName), nil, groupErr
+		}
+
+		sdlSum, err := sdlv1.Version(sdl)
+		if err != nil {
+			return simulation.NoOpMsg(types.ModuleName), nil, errors.Wrap(err, "error parsing deployment version sum")
+		}
+
 		account := ak.GetAccount(ctx, simAccount.Address)
 
 		fees, err := simulation.RandomFees(r, ctx, account.SpendableCoins(ctx.BlockTime()))
@@ -177,7 +197,12 @@ func SimulateMsgUpdateDeployment(ak govtypes.AccountKeeper, k keeper.Keeper) sim
 
 		msg := types.MsgUpdateDeployment{
 			ID:      deployment.ID(),
-			Version: simAccount.Address,
+			Groups:  make([]types.GroupSpec, 0, len(groupSpecs)),
+			Version: sdlSum,
+		}
+
+		for _, spec := range groupSpecs {
+			msg.Groups = append(msg.Groups, *spec)
 		}
 
 		tx := helpers.GenTx(
diff --git a/x/deployment/types/event.go b/x/deployment/types/event.go
index dde0f784..3f8ebddb 100644
--- a/x/deployment/types/event.go
+++ b/x/deployment/types/event.go
@@ -1,6 +1,7 @@
 package types
 
 import (
+	"encoding/hex"
 	"strconv"
 
 	sdk "github.com/cosmos/cosmos-sdk/types"
@@ -15,30 +16,37 @@ const (
 	evOwnerKey                = "owner"
 	evDSeqKey                 = "dseq"
 	evGSeqKey                 = "gseq"
+	evVersionKey              = "version"
+	encodedVersionHexLen      = 64
 )
 
 // EventDeploymentCreated struct
 type EventDeploymentCreated struct {
 	Context sdkutil.BaseModuleEvent `json:"context"`
 	ID      DeploymentID            `json:"id"`
+	Version []byte                  `json:"version"`
 }
 
-func NewEventDeploymentCreated(id DeploymentID) EventDeploymentCreated {
+// NewEventDeploymentCreated initializes creation event.
+func NewEventDeploymentCreated(id DeploymentID, version []byte) EventDeploymentCreated {
 	return EventDeploymentCreated{
 		Context: sdkutil.BaseModuleEvent{
 			Module: ModuleName,
 			Action: evActionDeploymentCreated,
 		},
-		ID: id,
+		ID:      id,
+		Version: version,
 	}
 }
 
 // ToSDKEvent method creates new sdk event for EventDeploymentCreated struct
 func (ev EventDeploymentCreated) ToSDKEvent() sdk.Event {
+	version := encodeHex(ev.Version)
 	return sdk.NewEvent(sdkutil.EventTypeMessage,
 		append([]sdk.Attribute{
 			sdk.NewAttribute(sdk.AttributeKeyModule, ModuleName),
 			sdk.NewAttribute(sdk.AttributeKeyAction, evActionDeploymentCreated),
+			sdk.NewAttribute(evVersionKey, string(version)),
 		}, DeploymentIDEVAttributes(ev.ID)...)...,
 	)
 }
@@ -47,25 +55,29 @@ func (ev EventDeploymentCreated) ToSDKEvent() sdk.Event {
 type EventDeploymentUpdated struct {
 	Context sdkutil.BaseModuleEvent `json:"context"`
 	ID      DeploymentID            `json:"id"`
-	Version []byte                  `json:"version,omitempty"` // TODO: #565
+	Version []byte                  `json:"version"`
 }
 
-func NewEventDeploymentUpdated(id DeploymentID) EventDeploymentUpdated {
+// NewEventDeploymentUpdated initializes SDK type
+func NewEventDeploymentUpdated(id DeploymentID, version []byte) EventDeploymentUpdated {
 	return EventDeploymentUpdated{
 		Context: sdkutil.BaseModuleEvent{
 			Module: ModuleName,
 			Action: evActionDeploymentUpdated,
 		},
-		ID: id,
+		ID:      id,
+		Version: version,
 	}
 }
 
 // ToSDKEvent method creates new sdk event for EventDeploymentUpdated struct
 func (ev EventDeploymentUpdated) ToSDKEvent() sdk.Event {
+	version := encodeHex(ev.Version)
 	return sdk.NewEvent(sdkutil.EventTypeMessage,
 		append([]sdk.Attribute{
 			sdk.NewAttribute(sdk.AttributeKeyModule, ModuleName),
 			sdk.NewAttribute(sdk.AttributeKeyAction, evActionDeploymentUpdated),
+			sdk.NewAttribute(evVersionKey, string(version)),
 		}, DeploymentIDEVAttributes(ev.ID)...)...,
 	)
 }
@@ -121,6 +133,28 @@ func ParseEVDeploymentID(attrs []sdk.Attribute) (DeploymentID, error) {
 	}, nil
 }
 
+// ParseEVDeploymentVersion returns the Deployment's SDL sha256 sum
+func ParseEVDeploymentVersion(attrs []sdk.Attribute) ([]byte, error) {
+	v, err := sdkutil.GetString(attrs, evVersionKey)
+	if err != nil {
+		return nil, err
+	}
+	version := decodeHex([]byte(v))
+	return version, nil
+}
+
+func encodeHex(src []byte) []byte {
+	dst := make([]byte, hex.EncodedLen(len(src)))
+	hex.Encode(dst, src)
+	return dst
+}
+
+func decodeHex(src []byte) []byte {
+	dst := make([]byte, hex.DecodedLen(len(src)))
+	hex.Decode(dst, src)
+	return dst
+}
+
 // EventGroupClosed provides SDK event to signal group termination
 type EventGroupClosed struct {
 	Context sdkutil.BaseModuleEvent `json:"context"`
@@ -173,7 +207,6 @@ func ParseEVGroupID(attrs []sdk.Attribute) (GroupID, error) {
 }
 
 // ParseEvent parses event and returns details of event and error if occurred
-// TODO: Enable returning actual events.
 func ParseEvent(ev sdkutil.Event) (sdkutil.ModuleEvent, error) {
 	if ev.Type != sdkutil.EventTypeMessage {
 		return nil, sdkutil.ErrUnknownType
@@ -187,13 +220,21 @@ func ParseEvent(ev sdkutil.Event) (sdkutil.ModuleEvent, error) {
 		if err != nil {
 			return nil, err
 		}
-		return NewEventDeploymentCreated(did), nil
+		ver, err := ParseEVDeploymentVersion(ev.Attributes)
+		if err != nil {
+			return nil, err
+		}
+		return NewEventDeploymentCreated(did, ver), nil
 	case evActionDeploymentUpdated:
 		did, err := ParseEVDeploymentID(ev.Attributes)
 		if err != nil {
 			return nil, err
 		}
-		return NewEventDeploymentUpdated(did), nil
+		ver, err := ParseEVDeploymentVersion(ev.Attributes)
+		if err != nil {
+			return nil, err
+		}
+		return NewEventDeploymentUpdated(did, ver), nil
 	case evActionDeploymentClosed:
 		did, err := ParseEVDeploymentID(ev.Attributes)
 		if err != nil {
diff --git a/x/deployment/types/msgs.go b/x/deployment/types/msgs.go
index 10d707b9..f8a8ff23 100644
--- a/x/deployment/types/msgs.go
+++ b/x/deployment/types/msgs.go
@@ -13,9 +13,9 @@ const (
 
 // MsgCreateDeployment defines an SDK message for creating deployment
 type MsgCreateDeployment struct {
-	ID DeploymentID `json:"id"`
-	// Version []byte      `json:"version"`
-	Groups []GroupSpec `json:"groups"`
+	ID      DeploymentID `json:"id"`
+	Groups  []GroupSpec  `json:"groups"`
+	Version []byte       `json:"version"`
 }
 
 // Route implements the sdk.Msg interface
@@ -42,14 +42,17 @@ func (msg MsgCreateDeployment) ValidateBasic() error {
 	if len(msg.Groups) == 0 {
 		return ErrInvalidGroups
 	}
-	// TODO: version
+	if len(msg.Version) == 0 {
+		return ErrEmptyVersion
+	}
 	return nil
 }
 
 // MsgUpdateDeployment defines an SDK message for updating deployment
 type MsgUpdateDeployment struct {
 	ID      DeploymentID
-	Version sdk.AccAddress
+	Groups  []GroupSpec
+	Version []byte
 }
 
 // Route implements the sdk.Msg interface
@@ -64,7 +67,7 @@ func (msg MsgUpdateDeployment) ValidateBasic() error {
 		return err
 	}
 
-	if err := sdk.VerifyAddressFormat(msg.Version); err != nil {
+	if len(msg.Version) == 0 {
 		return ErrEmptyVersion
 	}
 


Test patch:
diff --git a/events/publish_test.go b/events/publish_test.go
index 20bc9a54..73932968 100644
--- a/events/publish_test.go
+++ b/events/publish_test.go
@@ -15,8 +15,8 @@ import (
 func Test_processEvent(t *testing.T) {
 	tests := []sdkutil.ModuleEvent{
 		// x/deployment events
-		dtypes.NewEventDeploymentCreated(testutil.DeploymentID(t)),
-		dtypes.NewEventDeploymentUpdated(testutil.DeploymentID(t)),
+		dtypes.NewEventDeploymentCreated(testutil.DeploymentID(t), testutil.DeploymentVersion(t)),
+		dtypes.NewEventDeploymentUpdated(testutil.DeploymentID(t), testutil.DeploymentVersion(t)),
 		dtypes.NewEventDeploymentClosed(testutil.DeploymentID(t)),
 		dtypes.NewEventGroupClosed(testutil.GroupID(t)),
 
diff --git a/integration/deployment_test.go b/integration/deployment_test.go
index 7b931081..ab67228b 100644
--- a/integration/deployment_test.go
+++ b/integration/deployment_test.go
@@ -50,6 +50,16 @@ func TestDeployment(t *testing.T) {
 	require.NoError(t, err, "Error when fetching deployments with owner filter")
 	require.Len(t, deployments, 1)
 
+	// test updating deployment
+	execSuccess, stdOut, stdErr := f.TxUpdateDeployment(fmt.Sprintf("--from=%s --dseq=%d",
+		keyFoo, deployment.DeploymentID.DSeq), "-y")
+	require.True(t, execSuccess)
+	require.Empty(t, stdErr)
+	require.NotEmpty(t, stdOut)
+
+	deploymentV2 := f.QueryDeployment(createdDep.Deployment.DeploymentID)
+	require.NotEqual(t, deployment.Version, deploymentV2.Version)
+
 	// test query deployments with wrong owner value
 	deployments, err = f.QueryDeployments("--owner=cosmos102ruvpv2srmunfffxavttxnhezln6fnc3pf7tt")
 	require.Error(t, err)
diff --git a/integration/test_helpers.go b/integration/test_helpers.go
index 96a4b656..f86d165c 100644
--- a/integration/test_helpers.go
+++ b/integration/test_helpers.go
@@ -31,17 +31,18 @@ import (
 )
 
 const (
-	denom              = "akash"
-	denomStartValue    = 150
-	keyFoo             = "foo"
-	keyBar             = "bar"
-	keyBaz             = "baz"
-	fooDenom           = "footoken"
-	fooStartValue      = 1000
-	feeDenom           = "stake"
-	feeStartValue      = 1000000
-	deploymentFilePath = "./../x/deployment/testdata/deployment.yaml"
-	providerFilePath   = "./../x/provider/testdata/provider.yaml"
+	denom                = "akash"
+	denomStartValue      = 150
+	keyFoo               = "foo"
+	keyBar               = "bar"
+	keyBaz               = "baz"
+	fooDenom             = "footoken"
+	fooStartValue        = 1000
+	feeDenom             = "stake"
+	feeStartValue        = 1000000
+	deploymentFilePath   = "./../x/deployment/testdata/deployment.yaml"
+	deploymentV2FilePath = "./../x/deployment/testdata/deployment-v2.yaml"
+	providerFilePath     = "./../x/provider/testdata/provider.yaml"
 )
 
 var (
@@ -349,13 +350,19 @@ func (f *Fixtures) TxSend(from string, to sdk.AccAddress, amount sdk.Coin, flags
 //___________________________________________________________________________________
 // akash tx deployment
 
-// TxCreateDeployment is akash create deployment
+// TxCreateDeployment is akashctl create deployment
 func (f *Fixtures) TxCreateDeployment(flags ...string) (bool, string, string) {
 	cmd := fmt.Sprintf("%s tx deployment create %s %v %s", f.AkashBinary, deploymentFilePath, f.Flags(), f.KeyFlags())
 	return executeWriteRetStdStreams(f.T, addFlags(cmd, flags))
 }
 
-// TxCloseDeployment is akash close deployment
+// TxUpdateDeployment is akashctl update deployment
+func (f *Fixtures) TxUpdateDeployment(flags ...string) (bool, string, string) {
+	cmd := fmt.Sprintf("%s tx deployment update %s %v %s", f.AkashBinary, deploymentV2FilePath, f.Flags(), f.KeyFlags())
+	return executeWriteRetStdStreams(f.T, addFlags(cmd, flags))
+}
+
+// TxCloseDeployment is akashctl close deployment
 func (f *Fixtures) TxCloseDeployment(flags ...string) (bool, string, string) {
 	cmd := fmt.Sprintf("%s tx deployment close %v %s", f.AkashBinary, f.Flags(), f.KeyFlags())
 	return executeWriteRetStdStreams(f.T, addFlags(cmd, flags))
diff --git a/sdl/v1_test.go b/sdl/v1_test.go
index 3640c477..5beedd2a 100644
--- a/sdl/v1_test.go
+++ b/sdl/v1_test.go
@@ -3,7 +3,7 @@ package sdl_test
 import (
 	"testing"
 
-	"github.com/ovrclk/akash/sdl"
+	sdlv1 "github.com/ovrclk/akash/sdl"
 	"github.com/ovrclk/akash/types"
 	"github.com/ovrclk/akash/types/unit"
 	"github.com/stretchr/testify/assert"
@@ -16,18 +16,34 @@ const (
 	randStorage uint64 = 1 * unit.Gi
 )
 
-func Test_v1_Parse_docs(t *testing.T) {
-	sdl, err := sdl.ReadFile("../x/deployment/testdata/deployment.yaml")
+func Test_v1_Parse_Deployments(t *testing.T) {
+	sdl1, err := sdlv1.ReadFile("../x/deployment/testdata/deployment.yaml")
 	require.NoError(t, err)
-	_, err = sdl.DeploymentGroups()
+	_, err = sdl1.DeploymentGroups()
 	require.NoError(t, err)
 
-	_, err = sdl.Manifest()
+	_, err = sdl1.Manifest()
 	require.NoError(t, err)
+
+	sha1, err := sdlv1.Version(sdl1)
+	require.NoError(t, err)
+	assert.Len(t, sha1, 32)
+
+	sha2, err := sdlv1.Version(sdl1)
+	require.NoError(t, err)
+	assert.Len(t, sha2, 32)
+
+	require.Equal(t, sha1, sha2)
+
+	sdl2, err := sdlv1.ReadFile("../x/deployment/testdata/deployment-v2.yaml")
+	require.NoError(t, err)
+	sha3, err := sdlv1.Version(sdl2)
+	require.NoError(t, err)
+	require.NotEqual(t, sha1, sha3)
 }
 
 func Test_v1_Parse_simple(t *testing.T) {
-	sdl, err := sdl.ReadFile("./_testdata/simple.yaml")
+	sdl, err := sdlv1.ReadFile("./_testdata/simple.yaml")
 	require.NoError(t, err)
 
 	groups, err := sdl.DeploymentGroups()
@@ -80,7 +96,7 @@ func Test_v1_Parse_simple(t *testing.T) {
 }
 
 func Test_v1_Parse_ProfileNameNotServiceName(t *testing.T) {
-	sdl, err := sdl.ReadFile("./_testdata/profile-svc-name-mismatch.yaml")
+	sdl, err := sdlv1.ReadFile("./_testdata/profile-svc-name-mismatch.yaml")
 	require.NoError(t, err)
 
 	dgroups, err := sdl.DeploymentGroups()
diff --git a/testutil/deployment.go b/testutil/deployment.go
index 3a5021eb..6ff53cb8 100644
--- a/testutil/deployment.go
+++ b/testutil/deployment.go
@@ -1,18 +1,26 @@
 package testutil
 
 import (
+	"crypto/sha256"
 	"math/rand"
 	"testing"
 
 	dtypes "github.com/ovrclk/akash/x/deployment/types"
 )
 
+// sum256Seed provides a consistent sha256 value for initial Deployment.Version
+const sum256Seed = "hihi"
+
+// DefaultDeploymentVersion provides consistent sha256 sum for initial Deployment.Version
+var DefaultDeploymentVersion = sha256.Sum256([]byte(sum256Seed))
+
 // Deployment generates a dtype.Deployment in state `DeploymentActive`
 func Deployment(t testing.TB) dtypes.Deployment {
 	t.Helper()
 	return dtypes.Deployment{
 		DeploymentID: DeploymentID(t),
 		State:        dtypes.DeploymentActive,
+		Version:      DefaultDeploymentVersion[:],
 	}
 }
 
diff --git a/testutil/ids.go b/testutil/ids.go
index efe73c40..8a3eabcb 100644
--- a/testutil/ids.go
+++ b/testutil/ids.go
@@ -1,6 +1,8 @@
 package testutil
 
 import (
+	cryptorand "crypto/rand"
+	"crypto/sha256"
 	"math/rand"
 	"testing"
 
@@ -33,6 +35,18 @@ func DeploymentID(t testing.TB) dtypes.DeploymentID {
 	}
 }
 
+// DeploymentVersion provides a random sha256 sum for simulating Deployments.
+func DeploymentVersion(t testing.TB) []byte {
+	t.Helper()
+	src := make([]byte, 128)
+	_, err := cryptorand.Read(src)
+	if err != nil {
+		t.Fatal(err)
+	}
+	sum := sha256.Sum256(src)
+	return sum[:]
+}
+
 func GroupID(t testing.TB) dtypes.GroupID {
 	t.Helper()
 	return dtypes.MakeGroupID(DeploymentID(t), rand.Uint32())
diff --git a/x/deployment/handler/handler_test.go b/x/deployment/handler/handler_test.go
index 9fa61879..728ae176 100644
--- a/x/deployment/handler/handler_test.go
+++ b/x/deployment/handler/handler_test.go
@@ -1,12 +1,14 @@
 package handler_test
 
 import (
+	"crypto/sha256"
 	"testing"
 
 	"github.com/cosmos/cosmos-sdk/store"
 	sdk "github.com/cosmos/cosmos-sdk/types"
 	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	abci "github.com/tendermint/tendermint/abci/types"
 	dbm "github.com/tendermint/tm-db"
@@ -133,8 +135,9 @@ func TestUpdateDeploymentExisting(t *testing.T) {
 	deployment, groups := suite.createDeployment()
 
 	msg := types.MsgCreateDeployment{
-		ID:     deployment.ID(),
-		Groups: make([]types.GroupSpec, 0, len(groups)),
+		ID:      deployment.ID(),
+		Groups:  make([]types.GroupSpec, 0, len(groups)),
+		Version: testutil.DefaultDeploymentVersion[:],
 	}
 
 	for _, group := range groups {
@@ -145,8 +148,17 @@ func TestUpdateDeploymentExisting(t *testing.T) {
 	require.NoError(t, err)
 	require.NotNil(t, res)
 
+	t.Run("assert deployment version", func(t *testing.T) {
+		d, ok := suite.dkeeper.GetDeployment(suite.ctx, deployment.DeploymentID)
+		require.True(t, ok)
+		assert.Equal(t, d.Version, testutil.DefaultDeploymentVersion[:])
+	})
+
+	depSum := sha256.Sum256(testutil.DefaultDeploymentVersion[:])
+
 	msgUpdate := types.MsgUpdateDeployment{
-		ID: deployment.ID(),
+		ID:      deployment.ID(),
+		Version: depSum[:],
 	}
 
 	res, err = suite.handler(suite.ctx, msgUpdate)
@@ -161,6 +173,11 @@ func TestUpdateDeploymentExisting(t *testing.T) {
 
 		require.Equal(t, msg.ID, dev.ID)
 	})
+	t.Run("assert version updated", func(t *testing.T) {
+		d, ok := suite.dkeeper.GetDeployment(suite.ctx, deployment.DeploymentID)
+		require.True(t, ok)
+		assert.Equal(t, d.Version, depSum[:])
+	})
 }
 
 func TestCloseDeploymentNonExisting(t *testing.T) {
diff --git a/x/deployment/testdata/deployment-v2.yaml b/x/deployment/testdata/deployment-v2.yaml
new file mode 100644
index 00000000..75b6836d
--- /dev/null
+++ b/x/deployment/testdata/deployment-v2.yaml
@@ -0,0 +1,30 @@
+---
+version: "1.5"
+
+services:
+  web:
+    image: bubuntux/riot-web:v2
+    expose:
+      - port: 80
+        to:
+          - global: true
+
+profiles:
+  compute:
+    web:
+      cpu: "0.01"
+      memory: "128Mi"
+      storage: "512Mi"
+
+  placement:
+    global:
+      pricing:
+        web:
+          denom: akash
+          amount: 30
+
+deployment:
+  web:
+    global:
+      profile: web
+      count: 1
diff --git a/x/deployment/types/events_test.go b/x/deployment/types/events_test.go
index f439371c..8e947308 100644
--- a/x/deployment/types/events_test.go
+++ b/x/deployment/types/events_test.go
@@ -1,19 +1,23 @@
 package types
 
 import (
+	"crypto/sha256"
 	"strconv"
 	"testing"
 
 	sdk "github.com/cosmos/cosmos-sdk/types"
 	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
 	"github.com/ovrclk/akash/sdkutil"
 )
 
 var (
-	keyAcc, _   = sdk.AccAddressFromBech32("akash1qtqpdszzakz7ugkey7ka2cmss95z26ygar2mgr")
-	errWildcard = errors.New("wildcard string error can't be matched")
+	keyAcc, _         = sdk.AccAddressFromBech32("akash1qtqpdszzakz7ugkey7ka2cmss95z26ygar2mgr")
+	errWildcard       = errors.New("wildcard string error can't be matched")
+	tmpSum            = sha256.Sum256([]byte(keyAcc))
+	deploymentVersion = tmpSum[:]
 )
 
 type testEventParsing struct {
@@ -86,6 +90,10 @@ var TEPS = []testEventParsing{
 					Key:   evDSeqKey,
 					Value: "5",
 				},
+				{
+					Key:   evVersionKey,
+					Value: string(deploymentVersion),
+				},
 			},
 		},
 		expErr: nil,
@@ -122,6 +130,24 @@ var TEPS = []testEventParsing{
 		},
 		expErr: errWildcard,
 	},
+	{
+		msg: sdkutil.Event{
+			Type:   sdkutil.EventTypeMessage,
+			Module: ModuleName,
+			Action: evActionDeploymentCreated,
+			Attributes: []sdk.Attribute{
+				{
+					Key:   evOwnerKey,
+					Value: keyAcc.String(),
+				},
+				{
+					Key:   evDSeqKey,
+					Value: "5",
+				},
+			},
+		},
+		expErr: errWildcard,
+	},
 
 	{
 		msg: sdkutil.Event{
@@ -137,10 +163,32 @@ var TEPS = []testEventParsing{
 					Key:   evDSeqKey,
 					Value: "5",
 				},
+				{
+					Key:   evVersionKey,
+					Value: string(deploymentVersion),
+				},
 			},
 		},
 		expErr: nil,
 	},
+	{
+		msg: sdkutil.Event{
+			Type:   sdkutil.EventTypeMessage,
+			Module: ModuleName,
+			Action: evActionDeploymentUpdated,
+			Attributes: []sdk.Attribute{
+				{
+					Key:   evOwnerKey,
+					Value: keyAcc.String(),
+				},
+				{
+					Key:   evDSeqKey,
+					Value: "5",
+				},
+			},
+		},
+		expErr: errWildcard,
+	},
 	{
 		msg: sdkutil.Event{
 			Type:   sdkutil.EventTypeMessage,
@@ -163,6 +211,42 @@ var TEPS = []testEventParsing{
 		},
 		expErr: nil,
 	},
+	{
+		msg: sdkutil.Event{
+			Type:   sdkutil.EventTypeMessage,
+			Module: ModuleName,
+			Action: evActionDeploymentClosed,
+			Attributes: []sdk.Attribute{
+				{
+					Key:   evOwnerKey,
+					Value: keyAcc.String(),
+				},
+				{
+					Key:   evDSeqKey,
+					Value: "5",
+				},
+			},
+		},
+		expErr: nil,
+	},
+	{
+		msg: sdkutil.Event{
+			Type:   sdkutil.EventTypeMessage,
+			Module: ModuleName,
+			Action: evActionDeploymentClosed,
+			Attributes: []sdk.Attribute{
+				{
+					Key:   evOwnerKey,
+					Value: keyAcc.String(),
+				},
+				{
+					Key:   evDSeqKey,
+					Value: "abc",
+				},
+			},
+		},
+		expErr: errWildcard,
+	},
 	{
 		msg: sdkutil.Event{
 			Type:   sdkutil.EventTypeMessage,
@@ -213,6 +297,10 @@ var TEPS = []testEventParsing{
 					Key:   evDSeqKey,
 					Value: "5",
 				},
+				{
+					Key:   evVersionKey,
+					Value: string(deploymentVersion),
+				},
 			},
 		},
 		expErr: errWildcard,
@@ -238,3 +326,10 @@ func TestEventParsing(t *testing.T) {
 		t.Run(strconv.Itoa(i), test.testMessageType())
 	}
 }
+
+func TestVersionEncoding(t *testing.T) {
+	versionHex := encodeHex(deploymentVersion)
+	assert.Len(t, versionHex, encodedVersionHexLen)
+	decodedVersion := decodeHex(versionHex)
+	assert.Equal(t, deploymentVersion, decodedVersion)
+}
diff --git a/x/deployment/types/types_test.go b/x/deployment/types/types_test.go
index 8f9e4a32..3c4295d7 100644
--- a/x/deployment/types/types_test.go
+++ b/x/deployment/types/types_test.go
@@ -3,9 +3,15 @@ package types_test
 import (
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	abci "github.com/tendermint/tendermint/abci/types"
+
+	"github.com/ovrclk/akash/sdkutil"
 	"github.com/ovrclk/akash/testutil"
 	"github.com/ovrclk/akash/x/deployment/types"
-	"github.com/stretchr/testify/assert"
 )
 
 type gStateTest struct {
@@ -54,3 +60,48 @@ func TestGroupState(t *testing.T) {
 		assert.Equal(t, group.ValidateClosable(), test.expValidateClosable, group.State)
 	}
 }
+
+func TestDeploymentVersionAttributeLifecycle(t *testing.T) {
+	d := testutil.Deployment(t)
+
+	t.Run("deployment created", func(t *testing.T) {
+		edc := types.NewEventDeploymentCreated(d.ID(), d.Version)
+		sdkEvent := edc.ToSDKEvent()
+		strEvent := sdk.StringifyEvent(abci.Event(sdkEvent))
+
+		ev, err := sdkutil.ParseEvent(strEvent)
+		require.NoError(t, err)
+
+		versionString, err := types.ParseEVDeploymentVersion(ev.Attributes)
+		require.NoError(t, err)
+		assert.Equal(t, d.Version, versionString)
+	})
+
+	t.Run("deployment updated", func(t *testing.T) {
+		edu := types.NewEventDeploymentUpdated(d.ID(), d.Version)
+
+		sdkEvent := edu.ToSDKEvent()
+		strEvent := sdk.StringifyEvent(abci.Event(sdkEvent))
+
+		ev, err := sdkutil.ParseEvent(strEvent)
+		require.NoError(t, err)
+
+		versionString, err := types.ParseEVDeploymentVersion(ev.Attributes)
+		require.NoError(t, err)
+		assert.Equal(t, d.Version, versionString)
+	})
+
+	t.Run("deployment closed error", func(t *testing.T) {
+		edc := types.NewEventDeploymentClosed(d.ID())
+
+		sdkEvent := edc.ToSDKEvent()
+		strEvent := sdk.StringifyEvent(abci.Event(sdkEvent))
+
+		ev, err := sdkutil.ParseEvent(strEvent)
+		require.NoError(t, err)
+
+		versionString, err := types.ParseEVDeploymentVersion(ev.Attributes)
+		require.Error(t, err)
+		assert.NotEqual(t, d.Version, versionString)
+	})
+}