Everything so far has been about what a covenant does. KCC2 addresses a different question, and one that every piece of user-facing tooling has to answer:
Who controls this thing?
A wallet listing balances, an explorer showing an address page, an indexer filtering “covenants I care about” — all of them need to map a covenant UTXO to something a user recognizes. Without a convention, each one reverse-engineers each contract by hand.
KCC2 is small. It reuses KCC1’s types and decoding rules wholesale and standardizes only what is needed to discover the controlling value.
Three definitions
| Term | Meaning |
|---|---|
| Control principal | A principal associated with an application-declared control role |
| Principal reference | The typed KCC1 value that identifies or commits to that principal |
| Principal-reference profile | How that value is to be encoded and interpreted |
KCC2 deliberately uses “principal” rather than “identity” or “owner”. It is describing an ABI-level pointer, not defining what ownership means — the covenant’s actual authorization logic is its own business. A profile does not define the complete authorization predicate.
Control principal declarations
A Program ABI exposes zero or more declarations, each conveying three things:
ControlPrincipalDeclaration = (name, principal_location, profile_binding) name— an exact, case-sensitive, application-specific label, unique within the ABI.principal_location— a value location pointing at the value holding the current principal reference.profile_binding— how to interpret that value.
When an ABI declares several, it should list first the one observers should use by default.
The tuple notation here is explanatory. KCC2 does not prescribe an artifact syntax, only the information that must be conveyable.
Fixed and selected bindings
A binding is one of two shapes:
fixed(profile_id)
selected(selector_location, {
selector_value_0 => profile_id_0,
...
}) With a fixed binding, the declared profile is both supported and active.
With a selected binding, the case profiles are the supported set, and the case whose typed value equals the current selector value is active. Case values are values of the selector’s KCC1 type and must be distinct.
The important property: a selected binding describes an existing runtime choice. It does not add a tag to the program or change the state encoding. It documents a discriminator the contract already had.
name = example_role_a
principal_location = state.principal_references[0]
profile_binding = fixed(p2pk-schnorr/v1)
name = example_role_b
principal_location = state.alternate_principal_reference
profile_binding = selected(state.alternate_principal_kind, {
byte 00 => p2pkh-schnorr/v1,
byte 01 => program-hash/v1,
byte 02 => covenant-id/v1
}) An unmatched case is not an absent declaration
This distinction is easy to skim past and it changes how tooling should behave. An unknown profile, or a selector that matches no case, means the control principal’s profile is unsupported — not that the declaration is missing. Likewise an invalid location or an incompatible type makes the declaration invalid for the inspected program and state, not absent.
The practical difference: an observer should report “this covenant has an owner I cannot interpret” rather than “this covenant has no owner”. The second is a much stronger and usually wrong claim.
The initial profiles
| Profile | Compatible KCC1 value | Semantics |
|---|---|---|
p2pk-schnorr/v1 | pubkey | The 32-byte Schnorr public key itself |
p2pkh-schnorr/v1 | byte[32] containing P2PKHHash(pubkey) | Commitment to a 32-byte Schnorr public key |
p2pkh-ecdsa/v1 | byte[32] containing P2PKHHash(ecdsa_pubkey) | Commitment to a compressed 33-byte ECDSA key |
program-hash/v1 | byte[32] containing Hash(R) | One exact covenant program |
covenant-id/v1 | byte[32] containing a KIP-20 Covenant ID | One continuing covenant lineage |
Every one of these is a byte[32] (or pubkey, also 32 bytes). They are indistinguishable by type or by inspection — which is exactly why the profile has to be declared. This is the semantic annotation idea from KCC1 given a concrete, standardized vocabulary.
The version suffix fixes the interpretation without introducing a different KCC1 encoding.
P2PKHHash
The two P2PKH profiles use KCC1’s keyed hash with a dedicated domain-separation key:
P2PKHHash(pubkey) = Hash(pubkey, UTF8("PublicKeyHash")) Recall from KCC1 that the keyed form expands a key of at most 32 bytes by zero-padding on the right:
The same key serves both profiles. Their preimages stay unambiguous because p2pkh-schnorr/v1 requires exactly 32 bytes and p2pkh-ecdsa/v1 requires exactly 33 — so no input can be valid for both.
Worked example with a 32-byte Schnorr key of all 0x11:
pubkey = 1111111111111111111111111111111111111111111111111111111111111111
P2PKHHash(pubkey) = 0b3c683c1933a2be19c9dd05d2bc6f78de57a0cfd4418b5d1f3319599db68ab0 Covenant principals
Two of the profiles point at covenants rather than keys, and the difference between them is the difference between a program and a lineage:
program-hash/v1identifies one exact covenant program. It is a natural reference for a stateless covenant. It does not identify a unique UTXO — several UTXOs can hold the same program. And for a stateful covenant it is nearly useless, since the hash changes on every transition.covenant-id/v1identifies one continuing KIP-20 lineage. The program and state within that lineage may evolve freely while the Covenant ID stays stable.
For anything stateful, covenant-id/v1 is the reference you want. Discovering current lineage members, or following either reference into another covenant’s declarations, is an observer concern that KCC2 leaves open.
Inspecting a covenant
Given a UTXO and its Program ABI, a consumer that recognizes control principals:
- enumerates the control principal declarations;
- decodes each referenced root using KCC1;
- resolves the selector, when present, and chooses the active profile; and
- follows the principal path and reads the principal value.
For each declaration the result is: its name, supported profiles, active profile, principal value, and value location.
Two caching rules follow directly:
- A state-backed principal or selector must be re-read after every transition.
- An immutable principal is fixed for the inspected program — but if a continuation changes programs, the ABI must be inspected again.
KCC20 in these terms
KCC20 is a clean worked example of a selected binding, even though its own specification does not use KCC2’s vocabulary. Its required state begins:
owner_identifier: bytes32
identifier_type: byte with defined values 0x00 (pubkey), 0x01 (script hash), 0x02 (covenant ID).
Read as a KCC2 declaration, that is:
principal_location = state.owner_identifier
profile_binding = selected(state.identifier_type, {
byte 00 => p2pk-schnorr/v1,
byte 01 => program-hash/v1,
byte 02 => covenant-id/v1
}) The contract already had the discriminator, for its own authorization logic. KCC2 adds no bytes — it just gives observers a standard way to be told where to look.
Open question
KCC2 raises one explicitly: whether to allow custom profile expansions such as custom/qt-xmas-tree-v1, so community-defined profiles can exist before they become KCC-standard. Unresolved as of the current draft.
Next
KCC20 Fungible Tokens — the whole stack applied to a real standard.