Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6678c48
feat(registry): add multi-signer authorization controls (Phase 1 PoC)
vwagner Jun 18, 2026
3b1fc74
acceptance tests
vwagner Jun 18, 2026
c820f07
Add Option B single-signer role-change approval accumulation
vwagner Jun 18, 2026
6fd8f87
Revert registry role messages to single signer
vwagner Jun 18, 2026
3725eff
Add pending role-change queries, CLI, and genesis persistence
vwagner Jun 18, 2026
dea47f5
Add Controller revoke, invalidation, and edge-case acceptance tests
vwagner Jun 18, 2026
ddb43ac
reproduce behavior of SetRoles
vwagner Jun 23, 2026
26e1581
Complete ticket test matrix: scenario 2 single-signer rejects and aut…
vwagner Jun 23, 2026
8db7bbe
Address PR #1 review comments on registry authorization
vwagner Jun 23, 2026
ecf2d9b
Address second round of PR #1 review comments
vwagner Jun 23, 2026
8f28809
Address third round of PR #1 review comments
vwagner Jun 23, 2026
dd3c1ce
lint
vwagner Jun 23, 2026
3129f7d
Potential fix for pull request finding
vwagner Jun 23, 2026
a52570a
Merge pull request #2 from vwagner/vdub/sc-512248/hybrid-role-controls
vwagner Jun 24, 2026
ce99eeb
include gogo protos for local dev
vwagner Jun 24, 2026
133126d
feat(registry): add registry classes for asset-class-level authorization
vwagner Jun 24, 2026
87dca7d
feat(registry): add governance-managed module params for default role…
vwagner Jun 24, 2026
b8094c4
feat(registry): validate DART role policies as registry-class data (P…
vwagner Jun 24, 2026
0fa21fb
registry: add EventRoleUpdated event and ValidateRoleChange query (C4…
vwagner Jun 25, 2026
1dfcc1c
registry: address PR #4 review feedback
vwagner Jun 25, 2026
07484a3
registry: expand test coverage for class validation, query RPCs, and …
vwagner Jun 25, 2026
edefe5d
registry: address PR #4 review feedback (ledger callers, proto doc)
vwagner Jun 25, 2026
459c86e
registry: address PR #4 review feedback (event fail-fast, query id va…
vwagner Jun 25, 2026
892b4ae
registry: remove product-specific naming from examples and tests
vwagner Jun 25, 2026
cf83336
registry: parse validate-role-change role_updates via proto-JSON codec
vwagner Jun 25, 2026
9611340
registry: remove internal ticket references from comments and docs
vwagner Jun 25, 2026
1a533bb
fix(registry): enforce registry class asset-class match for entries
vwagner Jun 25, 2026
d2175d9
fix(registry): expose ValidateRoleChange over POST and fix CLI JSON e…
vwagner Jun 25, 2026
ecba3af
fix(registry): fail closed on missing registry class in authorization
vwagner Jun 25, 2026
cd404c8
PR feedback
vwagner Jun 25, 2026
acb234f
Merge pull request #4 from vwagner/vdub/sc-512248/configurable-controls
vwagner Jun 26, 2026
24e4588
Merge branch 'main' into vdub/registry-controls
vwagner Jun 26, 2026
0c75d57
fix(registry): enforce role policies in RegistryBulkUpdate; clear pen…
vwagner Jul 21, 2026
bcf3e08
fix(registry): require controller signature to unregister NFT
vwagner Jul 21, 2026
12e75e8
fix(registry): limit pending role changes to one per NFT
vwagner Jul 21, 2026
4504350
feat(registry): add CancelRoleChange and pending change expiry
vwagner Jul 21, 2026
8430750
fix(registry): address Cursor Bugbot review findings
vwagner Jul 21, 2026
f3310ac
feat(registry): add MsgAssociateRegistryClass for upgrading legacy en…
vwagner Jul 22, 2026
0dd4900
fix(registry): restrict AssociateRegistryClass scope auth to PARTY_TY…
vwagner Jul 22, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
* Add `MsgAssociateRegistryClass` to the registry module, allowing an existing legacy registry
entry to be upgraded to use a registry class without modifying roles. Authorization accepts any
of: CONTROLLER role address, scope data-owner party (for Provenance Metadata Scopes), or NFT
owner — neither takes precedence.
150 changes: 150 additions & 0 deletions proto/provenance/registry/v1/authorization.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
syntax = "proto3";
package provenance.registry.v1;

option go_package = "github.com/provenance-io/provenance/x/registry/types";
option java_package = "io.provenance.registry.v1";
option java_multiple_files = true;

import "gogoproto/gogo.proto";
import "cosmos_proto/cosmos.proto";
import "provenance/registry/v1/registry.proto";

// RegistryClass provides asset class-level authorization over role updates.
// It defines authorization rules for an entire NFT collection (asset class), allowing the
// maintainer to establish custom role update policies without requiring governance proposals.
message RegistryClass {
// registry_class_id is the unique identifier for this registry class (e.g. "loan-registry-v1").
// It is required since NFT classes do not have an inherent owner, and must be unique across
// all registry classes.
string registry_class_id = 1 [(gogoproto.jsontag) = "registryClassId,omitempty"];

// asset_class_id is the Scope Specification ID or NFT Class ID this registry class governs.
string asset_class_id = 2;

// maintainer is the address authorized to manage this registry class. It is permanent and
// cannot be changed once set.
string maintainer = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"];

// role_authorizations are the authorization rules for role updates within this asset class.
repeated RoleAuthorization role_authorizations = 4 [(gogoproto.nullable) = false];
}

// SignatureType defines how signature requirements are evaluated.
enum SignatureType {
// SIGNATURE_TYPE_UNSPECIFIED is the default/unset value.
SIGNATURE_TYPE_UNSPECIFIED = 0;
// SIGNATURE_TYPE_REQUIRED_ALL requires signatures from ALL listed roles.
// Fails immediately if any role does not exist in the registry or NFT state.
SIGNATURE_TYPE_REQUIRED_ALL = 1;
// SIGNATURE_TYPE_REQUIRED_ALL_IF_SET requires signatures from ALL listed roles,
// but only if they exist. Roles that are not set are silently skipped.
SIGNATURE_TYPE_REQUIRED_ALL_IF_SET = 2;
// SIGNATURE_TYPE_REQUIRED_ANY requires a signature from AT LEAST ONE of the listed roles.
// Fails if none of the roles exist.
SIGNATURE_TYPE_REQUIRED_ANY = 3;
// SIGNATURE_TYPE_REQUIRED_ANY_IF_SET requires a signature from AT LEAST ONE of the listed roles,
// but only if any of them exist. Skips the entire requirement if none of the roles are set.
SIGNATURE_TYPE_REQUIRED_ANY_IF_SET = 4;
}

// NftRole identifies roles managed outside the registry module (e.g., by the metadata module).
// These roles are read-only from the registry module's perspective.
enum NftRole {
// NFT_ROLE_UNSPECIFIED is the default/unset value.
NFT_ROLE_UNSPECIFIED = 0;
// NFT_ROLE_NFT_OWNER is the owner of the NFT.
// For metadata-scope NFTs, this is the scope value owner; otherwise it's the owner from the x/nft module.
NFT_ROLE_NFT_OWNER = 1;
}

// Assignment describes which address(es) to resolve for a role in an authorization check.
enum Assignment {
// ASSIGNMENT_UNSPECIFIED is the default/unset value.
ASSIGNMENT_UNSPECIFIED = 0;
// ASSIGNMENT_CURRENT resolves exactly one address currently assigned to the role.
// Fails if the role has multiple addresses assigned.
ASSIGNMENT_CURRENT = 1;
// ASSIGNMENT_CURRENT_ALL resolves all addresses currently assigned to the role.
// All of them must sign.
ASSIGNMENT_CURRENT_ALL = 2;
// ASSIGNMENT_CURRENT_ANY resolves any one of the addresses currently assigned to the role.
// At least one must sign.
ASSIGNMENT_CURRENT_ANY = 3;
// ASSIGNMENT_NEW resolves exactly one new address being assigned to the role.
// Fails if multiple new addresses are provided.
// Invalid for NftRole (read-only from registry perspective).
ASSIGNMENT_NEW = 4;
// ASSIGNMENT_NEW_ANY resolves any one of the new addresses being assigned to the role.
// Invalid for NftRole.
ASSIGNMENT_NEW_ANY = 5;
// ASSIGNMENT_NEW_ALL resolves all new addresses being assigned to the role.
// All of them must sign.
// Invalid for NftRole.
ASSIGNMENT_NEW_ALL = 6;
}

// RoleAuthorization configures who must sign to update a specific role.
// Contains one or more Authorization paths; the update is approved if ANY path is fully satisfied.
message RoleAuthorization {
// role is the registry role whose assignments are being controlled.
RegistryRole role = 1;

// authorizations is a list of alternative approval paths.
// The role update is approved if any single authorization is fully satisfied.
repeated Authorization authorizations = 2;
}

// Authorization defines one complete approval path for a role update.
// All signature requirements in this authorization must be satisfied for this path to succeed.
message Authorization {
// description is a human-readable explanation of this authorization path.
string description = 1;

// signatures is the ordered list of signature requirements that must all be satisfied.
repeated SignatureRequirement signatures = 2;
}

// SignatureRequirement defines a single signature check within an authorization path.
message SignatureRequirement {
// type controls how the roles are evaluated (required_all, required_all_if_set, etc.).
SignatureType type = 1;

// roles lists the role assignments that must be checked.
repeated RoleAssignment roles = 2;
}

// RoleAssignment specifies which role and which assignment type to resolve for a signature check.
message RoleAssignment {
// role_selector identifies the role to resolve, either a single role or a priority list.
oneof role_selector {
// registry_role is a role managed within the registry module state.
// Can be used with any assignment type.
RegistryRole registry_role = 1;
// nft_role is a role managed by another module (e.g., NFT_ROLE_NFT_OWNER from metadata).
// Can only be used with ASSIGNMENT_CURRENT* variants.
NftRole nft_role = 2;
// role_priority is an ordered list of roles; the first one that exists is used.
RolePriority role_priority = 3;
}

// assignment specifies which address(es) to resolve for the role.
Assignment assignment = 4;
}

// RolePriority is an ordered list of role entries used for fallback/hierarchical authority.
// The keeper uses the first role in the list that exists.
message RolePriority {
// entries is the ordered list of roles to check. The first existing role is used.
repeated RolePriorityEntry entries = 1;
}

// RolePriorityEntry is a single entry in a RolePriority list.
message RolePriorityEntry {
// role identifies the role in this priority slot.
oneof role {
// registry_role is a role managed within the registry module state.
RegistryRole registry_role = 1;
// nft_role is a role managed by another module.
NftRole nft_role = 2;
}
}
76 changes: 76 additions & 0 deletions proto/provenance/registry/v1/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ option go_package = "github.com/provenance-io/provenance/x/registry/typ
option java_package = "io.provenance.registry.v1";
option java_multiple_files = true;

import "gogoproto/gogo.proto";

// EventNFTRegistered is the event emitted when a registry is created for an NFT.
message EventNFTRegistered {
string nft_id = 1;
Expand Down Expand Up @@ -38,3 +40,77 @@ message EventRegistryBulkUpdated {
string nft_id = 1;
string asset_class_id = 2;
}

// EventRoleChangeProposed is emitted when a pending role change is opened.
message EventRoleChangeProposed {
string nft_id = 1;
string asset_class_id = 2;
string change_id = 3;
string proposer = 4;
}

// EventRoleChangeApproved is emitted when a party records an approval for a pending role change.
message EventRoleChangeApproved {
string change_id = 1;
string approver = 2;
}

// EventRoleChangeApplied is emitted when a pending role change accumulates enough approvals and is applied.
message EventRoleChangeApplied {
string nft_id = 1;
string asset_class_id = 2;
string change_id = 3;
}

// EventRoleChangeCancelled is emitted when a pending role change is cancelled by its proposer.
message EventRoleChangeCancelled {
string change_id = 1;
string cancelled_by = 2;
}

// EventRegistryClassCreated is emitted when a registry class is created.
message EventRegistryClassCreated {
string registry_class_id = 1;
string asset_class_id = 2;
string maintainer = 3;
}

// EventRegistryClassUpdated is emitted when a registry class's authorization rules are updated.
message EventRegistryClassUpdated {
string registry_class_id = 1;
string maintainer = 2;
}

// EventParamsUpdated is emitted when the registry module's params are updated via governance.
message EventParamsUpdated {}

// RoleSigner describes a role/assignment whose resolved addresses contributed a signature toward
// authorizing a role change. It is the structured form of an entry in EventRoleUpdated.signers.
message RoleSigner {
// role is the (registry or NFT) role whose holders provided the signature(s).
string role = 1;
// assignment is the assignment variant the role was resolved under (e.g. ASSIGNMENT_CURRENT).
string assignment = 2;
// addresses are the resolved addresses for the role that signed to authorize the change.
repeated string addresses = 3;
}

// EventRoleUpdated is the comprehensive event emitted whenever a role is granted, revoked, or set
// for a registered NFT. It is emitted for MsgGrantRole, MsgRevokeRole, and once per updated role
// for MsgSetRoles (including changes applied via the propose/approve accumulation flow).
message EventRoleUpdated {
// nft_id is the ID of the NFT for which the role was updated.
string nft_id = 1;
// asset_class_id is the ID of the asset class the NFT belongs to.
string asset_class_id = 2;
// registry_class_id is the registry class governing this NFT (empty if none).
string registry_class_id = 3;
// role is the role that was updated.
string role = 4;
// addresses are the addresses assigned to the role after this operation (empty if cleared).
repeated string addresses = 5;
// previous_addresses are the addresses assigned to the role before this operation.
repeated string previous_addresses = 6;
// signers describes the roles/addresses that authorized this change.
repeated RoleSigner signers = 7 [(gogoproto.nullable) = false];
}
12 changes: 12 additions & 0 deletions proto/provenance/registry/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,23 @@ option java_multiple_files = true;

import "gogoproto/gogo.proto";
import "provenance/registry/v1/registry.proto";
import "provenance/registry/v1/authorization.proto";
import "provenance/registry/v1/params.proto";

// GenesisState defines the registry module's genesis state.
// This contains all the registry entries that exist when the blockchain is first initialized.
message GenesisState {
// entries is the list of registry entries.
// These entries define the initial state of the registry module.
repeated RegistryEntry entries = 1 [(gogoproto.nullable) = false];

// pending_role_changes is the list of role changes awaiting approval.
// These preserve in-flight multi-party role changes across genesis export/import.
repeated PendingRoleChange pending_role_changes = 2 [(gogoproto.nullable) = false];

// registry_classes is the list of registry classes defining asset class-level authorization rules.
repeated RegistryClass registry_classes = 3 [(gogoproto.nullable) = false];

// params defines the registry module parameters at genesis.
Params params = 4 [(gogoproto.nullable) = false];
}
25 changes: 25 additions & 0 deletions proto/provenance/registry/v1/params.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
syntax = "proto3";
package provenance.registry.v1;

option go_package = "github.com/provenance-io/provenance/x/registry/types";
option java_package = "io.provenance.registry.v1";
option java_multiple_files = true;

import "gogoproto/gogo.proto";
import "provenance/registry/v1/authorization.proto";
import "google/protobuf/duration.proto";

// Params defines the registry module's parameters.
message Params {
// role_authorizations are the default authorization policies that govern role updates for
// registry entries that are not associated with a registry class. They form the middle tier of
// the two-tier resolution: a registry class policy takes precedence, then these module defaults,
// and finally roles without any policy fall back to legacy NFT-owner authorization.
repeated RoleAuthorization role_authorizations = 1 [(gogoproto.nullable) = false];

// pending_change_expiry is how long a pending role change remains valid after it is proposed.
// Approvals received after the expiry are rejected and the change is cleaned up.
// Zero value (default) means pending changes never expire.
google.protobuf.Duration pending_change_expiry = 2
[(gogoproto.stdduration) = true, (gogoproto.nullable) = false];
}
Loading