Skip to content
Merged
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
3 changes: 3 additions & 0 deletions pkg/api_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ type SubstrateGateway interface {
EnsureAccount(activationURL []string, termsAndConditionsLink string, termsAndConditionsHash string) (info substrate.AccountInfo, err error)
GetContract(id uint64) (substrate.Contract, SubstrateError)
GetContractIDByNameRegistration(name string) (uint64, SubstrateError)
// GetCouncilMembers returns the account ids of the current council members. Used to
// authorize council-driven (ops) deployment migrations without the owner's key.
GetCouncilMembers() ([]types.AccountID, SubstrateError)
GetFarm(id uint32) (substrate.Farm, error)
GetNode(id uint32) (substrate.Node, error)
GetNodeByTwinID(twin uint32) (uint32, SubstrateError)
Expand Down
9 changes: 6 additions & 3 deletions pkg/provision/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -1195,9 +1195,12 @@ func (n *NativeEngine) PrepareDeployment(twin uint32, deployment gridtypes.Deplo
return err
}

if err := deployment.Verify(n.twins); err != nil {
return err
}
// NOTE: no owner-signature check here (unlike CreateOrUpdate). A migration is authorized by
// the CHAIN: n.Prepare -> validate() enforces that the contract names THIS node and that its
// on-chain deployment_hash equals this deployment's ChallengeHash. Only the council can point
// a contract at a node and set that hash (migrate_node_contract), and the RMB caller was
// already authorized as the owner or a council member in the API handler. This lets ops move
// a VM without the owner's key — the deployment keeps its original owner twin.

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
Expand Down
17 changes: 17 additions & 0 deletions pkg/stubs/api_gateway_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,23 @@ func (s *SubstrateGatewayStub) GetContractIDByNameRegistration(ctx context.Conte
return
}

func (s *SubstrateGatewayStub) GetCouncilMembers(ctx context.Context) (ret0 []types.AccountID, ret1 pkg.SubstrateError) {
args := []interface{}{}
result, err := s.client.RequestContext(ctx, s.module, s.object, "GetCouncilMembers", args...)
if err != nil {
panic(err)
}
result.PanicOnError()
loader := zbus.Loader{
&ret0,
&ret1,
}
if err := result.Unmarshal(&loader); err != nil {
panic(err)
}
return
}

func (s *SubstrateGatewayStub) GetFarm(ctx context.Context, arg0 uint32) (ret0 tfchainclientgo.Farm, ret1 error) {
args := []interface{}{arg0}
result, err := s.client.RequestContext(ctx, s.module, s.object, "GetFarm", args...)
Expand Down
31 changes: 31 additions & 0 deletions pkg/substrate_gateway/substrate_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,37 @@ func (g *substrateGateway) GetZosVersion() (string, error) {
return result, err
}

func (g *substrateGateway) GetCouncilMembers() (result []types.AccountID, serr pkg.SubstrateError) {
log.Trace().Str("method", "GetCouncilMembers").Msg("method called")

err := backoff.Retry(func() error {
cl, meta, retryErr := g.sub.GetClient()
if retryErr != nil {
return retryErr
}
key, retryErr := types.CreateStorageKey(meta, "Council", "Members")
if retryErr != nil {
// a metadata/pallet-name problem won't fix itself on retry
return backoff.Permanent(retryErr)
}
var members []types.AccountID
ok, retryErr := cl.RPC.State.GetStorageLatest(key, &members)
if retryErr != nil {
log.Debug().Err(retryErr).Msg("GetCouncilMembers failed, retrying")
return retryErr
}
if !ok {
result = nil
return nil
}
result = members
return nil
}, createBackoff())

serr = buildSubstrateError(err)
return
}

func (g *substrateGateway) CreateNode(node substrate.Node) (uint32, error) {
log.Debug().
Str("method", "CreateNode").
Expand Down
83 changes: 78 additions & 5 deletions pkg/zos_api/deployment.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package zosapi

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -62,8 +63,22 @@ func (g *ZosAPI) deploymentGetHandler(ctx context.Context, payload []byte) (inte
return nil, err
}

return g.provisionStub.Get(ctx, peer.GetTwinID(ctx), args.ContractID)

// Fast path (unchanged behavior): the caller reads its own deployment — no chain lookup.
twin := peer.GetTwinID(ctx)
if dl, err := g.provisionStub.Get(ctx, twin, args.ContractID); err == nil {
return dl, nil
}
// Slow path: not the caller's own deployment. Allow an ops/council read of another owner's
// deployment (needed to drive a keyless migration) — resolve the owner from the on-chain
// contract and authorize the caller as the owner or a council member.
owner, oerr := g.ownerOfContract(ctx, args.ContractID)
if oerr != nil {
return nil, oerr
}
if aerr := g.authorizeMigration(ctx, owner); aerr != nil {
return nil, aerr
}
return g.provisionStub.Get(ctx, owner, args.ContractID)
}

func (g *ZosAPI) deploymentListHandler(ctx context.Context, payload []byte) (interface{}, error) {
Expand All @@ -86,6 +101,42 @@ func (g *ZosAPI) deploymentChangesHandler(ctx context.Context, payload []byte) (
// source deployment for a consistent copy, then uploads each requested workload
// to a caller-provided presigned S3 URL (HTTP PUT). Used on the OLD node during
// a contract move.
// authorizeMigration authorizes a council-driven (ops) migration op on a deployment owned by
// ownerTwin. The RMB caller must be either the owner itself or a current council member —
// council already governs the on-chain contract move (migrate_node_contract), so it may also
// drive the node-side data move. This is what lets ops migrate a VM without the owner's key.
// It returns nil if authorized.
func (g *ZosAPI) authorizeMigration(ctx context.Context, ownerTwin uint32) error {
caller := peer.GetTwinID(ctx)
if caller == ownerTwin {
return nil
}
callerTwin, err := g.substrateGatewayStub.GetTwin(ctx, caller)
if err != nil {
return fmt.Errorf("failed to resolve caller twin %d: %w", caller, err)
}
members, serr := g.substrateGatewayStub.GetCouncilMembers(ctx)
if serr.IsError() {
return fmt.Errorf("failed to fetch council members: %w", serr.Err)
}
callerPk := callerTwin.Account.PublicKey()
for _, m := range members {
if bytes.Equal(m[:], callerPk) {
return nil
}
}
return fmt.Errorf("caller twin %d is neither the deployment owner (twin %d) nor a council member", caller, ownerTwin)
}

// ownerOfContract resolves the owner twin of a node contract from chain.
func (g *ZosAPI) ownerOfContract(ctx context.Context, contractID uint64) (uint32, error) {
contract, serr := g.substrateGatewayStub.GetContract(ctx, contractID)
if serr.IsError() {
return 0, fmt.Errorf("failed to get contract %d: %w", contractID, serr.Err)
}
return uint32(contract.TwinID), nil
}

func (g *ZosAPI) deploymentTransferHandler(ctx context.Context, payload []byte) (interface{}, error) {
var args struct {
ContractID uint64 `json:"contract_id"`
Expand All @@ -95,7 +146,16 @@ func (g *ZosAPI) deploymentTransferHandler(ctx context.Context, payload []byte)
return nil, err
}

twin := peer.GetTwinID(ctx)
// Owner-agnostic: resolve the owner from the contract and authorize the caller as the
// owner or a council member. All storage ops below use the OWNER twin (deployments are
// stored per-owner), so an ops/council caller can transfer a VM it does not own.
twin, err := g.ownerOfContract(ctx, args.ContractID)
if err != nil {
return nil, err
}
if err := g.authorizeMigration(ctx, twin); err != nil {
return nil, err
}
deployment, err := g.provisionStub.Get(ctx, twin, args.ContractID)
if err != nil {
return nil, err
Expand Down Expand Up @@ -137,7 +197,13 @@ func (g *ZosAPI) deploymentPrepareHandler(ctx context.Context, payload []byte) (
return nil, err
}

twin := peer.GetTwinID(ctx)
// The deployment carries its owner twin; authorize the caller as the owner or a council
// member (the on-chain contract hash, set by the council move, is the real authority — see
// engine.validate). The owner twin is used for the (per-owner) provisioning.
twin := args.Deployment.TwinID
if err := g.authorizeMigration(ctx, twin); err != nil {
return nil, err
}

// resolve + validate the requested workloads up front (synchronous errors)
jobs, err := resolveTransferJobs(&args.Deployment, args.Downloads)
Expand Down Expand Up @@ -168,7 +234,14 @@ func (g *ZosAPI) deploymentStartHandler(ctx context.Context, payload []byte) (in
if err := json.Unmarshal(payload, &args); err != nil {
return nil, err
}
return nil, g.provisionStub.StartDeployment(ctx, peer.GetTwinID(ctx), args.ContractID)
twin, err := g.ownerOfContract(ctx, args.ContractID)
if err != nil {
return nil, err
}
if err := g.authorizeMigration(ctx, twin); err != nil {
return nil, err
}
return nil, g.provisionStub.StartDeployment(ctx, twin, args.ContractID)
}

// waitWorkloadProvisioned blocks until the workload with the given global id
Expand Down
2 changes: 2 additions & 0 deletions pkg/zos_api/zos_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type ZosAPI struct {
statisticsStub *stubs.StatisticsStub
storageStub *stubs.StorageModuleStub
performanceMonitorStub *stubs.PerformanceMonitorStub
substrateGatewayStub *stubs.SubstrateGatewayStub
diagnosticsManager *diagnostics.DiagnosticsManager
farmerID uint32
inMemCache *cache.Cache
Expand All @@ -56,6 +57,7 @@ func NewZosAPI(manager substrate.Manager, client zbus.Client, msgBrokerCon strin
statisticsStub: stubs.NewStatisticsStub(client),
storageStub: storageModuleStub,
performanceMonitorStub: stubs.NewPerformanceMonitorStub(client),
substrateGatewayStub: stubs.NewSubstrateGatewayStub(client),
diagnosticsManager: diagnosticsManager,
}
exp := backoff.NewExponentialBackOff()
Expand Down
78 changes: 73 additions & 5 deletions pkg/zos_api_light/deployment.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package zosapi

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -64,8 +65,20 @@ func (g *ZosAPI) deploymentGetHandler(ctx context.Context, payload []byte) (inte
return nil, err
}

return g.provisionStub.Get(ctx, peer.GetTwinID(ctx), args.ContractID)

// fast path (unchanged): the caller reads its own deployment — no chain lookup
twin := peer.GetTwinID(ctx)
if dl, err := g.provisionStub.Get(ctx, twin, args.ContractID); err == nil {
return dl, nil
}
// slow path: allow an ops/council read of another owner's deployment (keyless migration)
owner, oerr := g.ownerOfContract(ctx, args.ContractID)
if oerr != nil {
return nil, oerr
}
if aerr := g.authorizeMigration(ctx, owner); aerr != nil {
return nil, aerr
}
return g.provisionStub.Get(ctx, owner, args.ContractID)
}

func (g *ZosAPI) deploymentListHandler(ctx context.Context, payload []byte) (interface{}, error) {
Expand All @@ -88,6 +101,41 @@ func (g *ZosAPI) deploymentChangesHandler(ctx context.Context, payload []byte) (
// source deployment for a consistent copy, then uploads each requested workload
// to a caller-provided presigned S3 URL (HTTP PUT). Used on the OLD node during
// a contract move.
// authorizeMigration authorizes a council-driven (ops) migration op on a deployment owned by
// ownerTwin: the RMB caller must be the owner or a current council member (council governs the
// on-chain contract move, so it may also drive the node-side data move). Lets ops migrate a VM
// without the owner's key.
func (g *ZosAPI) authorizeMigration(ctx context.Context, ownerTwin uint32) error {
caller := peer.GetTwinID(ctx)
if caller == ownerTwin {
return nil
}
callerTwin, err := g.substrateGatewayStub.GetTwin(ctx, caller)
if err != nil {
return fmt.Errorf("failed to resolve caller twin %d: %w", caller, err)
}
members, serr := g.substrateGatewayStub.GetCouncilMembers(ctx)
if serr.IsError() {
return fmt.Errorf("failed to fetch council members: %w", serr.Err)
}
callerPk := callerTwin.Account.PublicKey()
for _, m := range members {
if bytes.Equal(m[:], callerPk) {
return nil
}
}
return fmt.Errorf("caller twin %d is neither the deployment owner (twin %d) nor a council member", caller, ownerTwin)
}

// ownerOfContract resolves the owner twin of a node contract from chain.
func (g *ZosAPI) ownerOfContract(ctx context.Context, contractID uint64) (uint32, error) {
contract, serr := g.substrateGatewayStub.GetContract(ctx, contractID)
if serr.IsError() {
return 0, fmt.Errorf("failed to get contract %d: %w", contractID, serr.Err)
}
return uint32(contract.TwinID), nil
}

func (g *ZosAPI) deploymentTransferHandler(ctx context.Context, payload []byte) (interface{}, error) {
var args struct {
ContractID uint64 `json:"contract_id"`
Expand All @@ -97,7 +145,15 @@ func (g *ZosAPI) deploymentTransferHandler(ctx context.Context, payload []byte)
return nil, err
}

twin := peer.GetTwinID(ctx)
// owner-agnostic: resolve the owner from the contract and authorize the caller as owner or
// council; all storage ops below run under the OWNER twin.
twin, err := g.ownerOfContract(ctx, args.ContractID)
if err != nil {
return nil, err
}
if err := g.authorizeMigration(ctx, twin); err != nil {
return nil, err
}
deployment, err := g.provisionStub.Get(ctx, twin, args.ContractID)
if err != nil {
return nil, err
Expand Down Expand Up @@ -139,7 +195,12 @@ func (g *ZosAPI) deploymentPrepareHandler(ctx context.Context, payload []byte) (
return nil, err
}

twin := peer.GetTwinID(ctx)
// the deployment carries its owner twin; authorize the caller as owner or council. The
// on-chain contract hash (set by the council move) is the authority — see engine.validate.
twin := args.Deployment.TwinID
if err := g.authorizeMigration(ctx, twin); err != nil {
return nil, err
}

// resolve + validate the requested workloads up front (synchronous errors)
jobs, err := resolveTransferJobs(&args.Deployment, args.Downloads)
Expand Down Expand Up @@ -170,7 +231,14 @@ func (g *ZosAPI) deploymentStartHandler(ctx context.Context, payload []byte) (in
if err := json.Unmarshal(payload, &args); err != nil {
return nil, err
}
return nil, g.provisionStub.StartDeployment(ctx, peer.GetTwinID(ctx), args.ContractID)
twin, err := g.ownerOfContract(ctx, args.ContractID)
if err != nil {
return nil, err
}
if err := g.authorizeMigration(ctx, twin); err != nil {
return nil, err
}
return nil, g.provisionStub.StartDeployment(ctx, twin, args.ContractID)
}

// waitWorkloadProvisioned blocks until the workload with the given global id
Expand Down
3 changes: 3 additions & 0 deletions pkg/zos_api_light/zos_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type ZosAPI struct {
statisticsStub *stubs.StatisticsStub
storageStub *stubs.StorageModuleStub
performanceMonitorStub *stubs.PerformanceMonitorStub
substrateGatewayStub *stubs.SubstrateGatewayStub
diagnosticsManager *diagnostics.DiagnosticsManager
farmerID uint32
inMemCache *cache.Cache
Expand All @@ -54,6 +55,7 @@ func NewZosAPI(manager substrate.Manager, client zbus.Client, msgBrokerCon strin
statisticsStub: stubs.NewStatisticsStub(client),
storageStub: storageModuleStub,
performanceMonitorStub: stubs.NewPerformanceMonitorStub(client),
substrateGatewayStub: stubs.NewSubstrateGatewayStub(client),
diagnosticsManager: diagnosticsManager,
}
exp := backoff.NewExponentialBackOff()
Expand Down Expand Up @@ -109,6 +111,7 @@ func NewZosAPIWithFarmerID(client zbus.Client, farmerID uint32, msgBrokerCon str
statisticsStub: stubs.NewStatisticsStub(client),
storageStub: storageModuleStub,
performanceMonitorStub: stubs.NewPerformanceMonitorStub(client),
substrateGatewayStub: stubs.NewSubstrateGatewayStub(client),
diagnosticsManager: diagnosticsManager,
}
api.farmerID = farmerID
Expand Down
9 changes: 7 additions & 2 deletions scripts/qsfs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ const (
QSFSCacheSize = 1 // GB

SSHKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTwULSsUubOq3VPWL6cdrDvexDmjfznGydFPyaNcn7gAL9lRxwFbCDPMj7MbhNSpxxHV2+/iJPQOTVJu4oc1N7bPP3gBCnF51rPrhTpGCt5pBbTzeyNweanhedkKDsCO2mIEh/92Od5Hg512dX4j7Zw6ipRWYSaepapfyoRnNSriW/s3DH/uewezVtL5EuypMdfNngV/u2KZYWoeiwhrY/yEUykQVUwDysW/xUJNP5o+KSTAvNSJatr3FbuCFuCjBSvageOLHePTeUwu6qjqe+Xs4piF1ByO/6cOJ8bt5Vcx0bAtI8/MPApplUU/JWevsPNApvnA/ntffI+u8DCwgP ashraf@thinkpad"

Mnemonic = "junior sock chunk accident pilot under ask green endless remove coast wood"
)

// Mnemonic is the owner twin mnemonic — read from the MNEMONIC env var, never hard-coded.
var Mnemonic = os.Getenv("MNEMONIC")

// generateWGPrivateKey generates a WireGuard (Curve25519) private key
func generateWGPrivateKey() string {
var key [32]byte
Expand Down Expand Up @@ -343,6 +344,10 @@ func main() {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")

if Mnemonic == "" {
panic("set the MNEMONIC env var (owner twin mnemonic)")
}

identity, err := substrate.NewIdentityFromSr25519Phrase(Mnemonic)
if err != nil {
panic(err)
Expand Down
Loading