7. Accounts and authorization

Draft protocolSource at 169cbd2 ↗Excerpt SHA-256 358bb8b…daf

An account is an identity that owns projects, holds credits, and authorizes commands. This chapter defines how an account's identity is derived, how its root authority is held and reconfigured, how that root delegates signing power to ordinary keys, what a signature over a command binds, and how replay and authority context are prevented from reviving a stale signature. The commands that carry these transitions are cataloged in section 8; what they cost is in section 10.

7.1 Identity

An account's AccountId is the 20-byte address of its genesis authority. There are two genesis forms. A single-key account is addressed by the Ethereum address of one secp256k1 key, as on Tempo. A configured account is addressed by its genesis owner configuration, derived exactly as Tempo's configurable accounts are, following the TIP-1107 cluster: TIP-1110 for the address, TIP-1114 for the configuration, and TIP-1108 for the commitment. The adopted TIP text is pinned to the specification revision at the commit named in the chainspec.

account_salt = keccak256("tempo:multisig:account" || salt || threshold || owner_count || owners…)
AccountId    = CREATE2(MULTISIG_RECOVERY_FACTORY, account_salt, MULTISIG_RECOVERY_WALLET_INIT_CODE_HASH)[12..32]
factory      = chainspec value `multisig_recovery_factory`, unset until Tempo publishes it
init hash    = 0x583cc63a2e37f645b43eac911b1a6d6de08b83abdc308c61364edda8cfc3bd37

The canonical preimage byte order and the adopted TIP revisions are fixed by encoding section 3.3. Adopting those derivations asserts nothing about whether the named TIPs are merged into or active on a Tempo network.

R-7.1. AccountId never changes.

R-7.2. A genesis form is verified by recomputation: a single-key account from its secp256k1 key, and a configured account from the owner configuration supplied as its genesis witness.

R-7.3. A validator MUST NOT read Tempo state to establish a Makechain identity or authority.

R-7.4. A derived AccountId equal to the zero address is invalid.

R-7.5. While the chainspec value multisig_recovery_factory is unset, the configured genesis form is deterministically invalid.

R-7.6. multisig_recovery_factory is set at most once, by a governed set-once release parameter carrying Tempo's published constant (section 18), and MUST NOT change after it is set.

R-7.7. The multisig_recovery_factory value set by a release MUST be nonzero.

R-7.8. No Makechain account action depends on Tempo finality, on a relayer, or on the state of the settlement bridge.

R-7.9. A key removed on one chain keeps its Makechain authority until its owners remove it here as well, and clients SHOULD compare the two configurations off chain and prompt the owners when they differ; that advisory involves no consensus.

R-7.10. A third-party funder can never set account policy, and paying for another account's username extension never changes the beneficiary, the current-name pointer, or the stored deposit payer (sections 8 and 10).

While the factory is unset, every account begins as a single-key account and reconfigures in place (section 7.5), which needs no factory because the account keeps its key's address. Once it is set, a Tempo configurable account and its Makechain account are one identity, so credits deposited to a Tempo quorum account's own address are controlled on Makechain by the same owners. An account's current authority is state, held in AccountRoot (section 7.2), so owners and threshold change without changing the account; after genesis the two chains' configurations are independent. Tempo is the on-ramp for credits and the reference for identity derivation and configuration bounds, never the source of truth for Makechain authority. The address derivation above, the config_commitment of section 7.2, and the in-place upgrade salt of section 7.5 are the three pinned Tempo formulas: they use Keccak because that is Tempo's format, and they are not part of the proof path of section 6.

An account exists in two stages. A settlement proof may create a minimal credit ledger for any address (section 16), and INIT_ACCOUNT then initializes policy, signed by the genesis authority with its genesis witness and charged like any account-created record.

INV-AUTH-01. AccountId is immutable; reconfiguring the root authority changes the owners and threshold, never the identity.

INV-AUTH-10. Third-party username extension never restores resolution or changes ownership, the current-name pointer, or the deposit payer.

7.2 Root authority

Root authority is a weighted quorum of owners, stored as a commitment. The AccountRoot record holds that commitment and the account's authority counters, and every initialized account has one. The owner configuration itself is not stored: it travels as a witness with every root-authorized command, so per-account authority state is one record regardless of owner count.

AccountRoot { config_commitment, version, root_sequence, delegation_epoch,
              key_allocation_counter, epoch_start_key_counter, keychain_revision }

OwnerConfiguration { salt, version, threshold, owners[1..48]: { owner, weight: u8 } }

Canonical field order and value layout are fixed by encoding section 3.3 and encoding section 6. Each configuration entry is a raw 20-byte owner address and a weight; a key family and a full public key belong to the approval witness that derives that address, never to the configuration's identity preimage. The bounds follow Tempo's:

Element Bound
owners from 1 to 48 owners, both inclusive, sorted by address and unique
weight nonzero, and all weights summing to at most 255
threshold from 1 to 8, both inclusive, and never exceeding the total weight
owner entry the address of a secp256k1, P256, or WebAuthn key; never an Ed25519 key, and never another configured account, since nested owner accounts are rejected as on Tempo and are admitted only by a later release if Tempo admits them

R-7.11. A configuration is valid only if it satisfies every bound of the table above.

R-7.12. A version-zero configuration MUST NOT list its own derived address.

R-7.13. A configuration at a positive version MAY list the account's original key address, so a single-key account can upgrade to a 1-of-1 configuration of its existing key and rotate later.

R-7.14. The configuration carried by a root-authorized command MUST be checked against config_commitment in the account's AccountRoot.

R-7.15. config_commitment is Tempo's config_commitment value computed Tempo's way, which is a Keccak digest, so that a future storage-proof synchronization compares equal.

R-7.16. A single-key account's AccountRoot holds version = 0, the root replay mark, and an unset commitment, and its genesis key is the 1-of-1 authority until the first ROOT_CONFIGURE sets a commitment (section 7.5).

R-7.17. Root authority administers the keychain and is the only authority that can authorize, renew, revoke, or reconfigure.

The root signs through a slot of its own, key_id = BLAKE3("makechain.root-slot" || AccountId), which is account-scoped and backed by root_sequence.

R-7.18. The root signing slot is never a delegated record, never selects one, and cannot be invalidated by bulk revocation (section 7.3).

R-7.19. A root envelope's authorization_generation MUST equal the executing account's configuration version, which is zero for a single-key account.

R-7.20. Root authorization and genesis authorization use the permanent root_sequence mark, and not a fresh mark per configuration version.

R-7.21. INIT_ACCOUNT is valid only with a positive sequence, and that accepted sequence is stored in the new AccountRoot.

An Ed25519 key is never an owner, so one configuration derives the same identity on both chains.

7.3 Delegated keys

A delegated key is a record in an account's keychain that may authorize commands on the account's behalf. Each delegated key carries a capability class, a family and its verification policy, an optional validity window, expires_at_ms, an authorization_generation, and an issued_delegation_epoch. The account's delegation epoch is a permanent counter that bulk revocation advances. A canonical key identity binds the owning AccountId, the key family, and the full public key, so the same public key used by two accounts never shares a replay mark or a pool entry.

R-7.22. Capability classes nest: ACCOUNT ⊇ PROJECT ⊇ WRITE ⊇ SOCIAL.

R-7.23. A key of class SOCIAL authorizes no command and exists only for off-chain signing (section 8).

R-7.24. A delegated key may authorize a command only if its issued_delegation_epoch equals the account's current delegation epoch and its time predicates hold on consensus block time (section 9).

R-7.25. Changing class, scope, family policy, origins, or validity-window bounds requires a new authorization generation.

R-7.71. A validity window whose start is not strictly before its end is invalid.

An optional project scope narrows a key to one project.

R-7.26. At launch a project_scope is permitted only on a key of class WRITE, is an immutable project id for that authorization generation, and restricts the key as follows:

a. on-chain use is restricted to PUSH for the named project; b. every ref operation for that project is permitted, including force updates and deletes; c. project administration and RECLAIM are never authorized; d. a push is valid only if the account holds a current writing role and the project is ACTIVE (section 8); e. the inherited SOCIAL capability is restricted as specified in section 8.

Scope restricts authority and not spending: a scoped WRITE key can still consume its account's available credits through valid pushes to that project. A delegated key carries no spending allowance at launch, so a leaked key of sufficient class can spend within its class (section 4).

Key families, their wire forms, and their verification policy:

Family Wire form Role Verification policy
secp256k1 r ‖ s ‖ v, low-S owner or root; the only single-key genesis form Ethereum address derivation, external crate
Ed25519 SSHSIG 64-byte signature, with the SSHSIG outer fields reconstructed by the verifier delegated; never an owner software SSH keys through ssh-keygen -Y sign and ssh-agent; signed preimage, hash algorithm, namespace, and permitted key types pinned to the SSHSIG profile; FIDO sk-ssh-ed25519 keys have a different signature container and are a later profile
P256 direct r ‖ s, low-S owner or delegated Secure Enclave and HSM keys, signing a domain-prefixed preimage with one SHA-256 step (section 7.4)
WebAuthn P256 authenticatorData ‖ clientDataJSON ‖ r ‖ s, low-S normalized from DER owner or delegated rpIdHash and authorized origins are pinned at KEY_AUTHORIZE for a delegated key and checked against the root WebAuthn policy of section 7.4 for an owner; user-presence and user-verification policy fixed; the signature is verified over the authenticator data and the hash of the exact received client-data bytes, never reserialized; client-data verification is limited, uses a bounded grammar, and rejects duplicate fields; authenticator counters are ignored, because the chain sequence is the replay protection (section 7.6)

Byte layouts are fixed by encoding section 3.2.

R-7.27. A signature is valid only if it satisfies its family's verification policy in the table above.

R-7.28. Every family has an exact encoded-size bound, and a stated 64-byte signature describes only the inner signature and never the envelope.

R-7.29. The SSHSIG, WebAuthn, and P256-direct families are verified with external standard verifiers over the same underlying curve crates, while the library verifier is used for consensus and DKG signatures (section 13).

R-7.30. Native and wasm verification MUST agree on every vector.

R-7.31. Signer-generated vectors from real tools and authenticators, including negative origin, challenge, flag, and malformed-signature cases, are required before any family is frozen.

Those external standards fix their own signed preimages, while the library's public verifiers always prepend a namespace, and P256 uses different backends by target.

The keychain's lifecycle transitions are carried by KEY_AUTHORIZE and KEY_REVOKE, cataloged in section 8. Their effect on keychain state is:

Variant Valid only if Effect
KEY_AUTHORIZE(Authorize) the payload binds the predecessor's expected generation or its absence, and any resident predecessor has already been invalidated by its expiry, its validity-window end, or its issuing epoch; a predecessor invalidated by none of these, including one whose validity window has not started, MUST first be explicitly revoked allocates a fresh generation from key_allocation_counter, stamps the current delegation epoch, initializes the generation's replay mark, charges the creation fee and locks the deposit (sections 9 and 10), and reads a resident predecessor raw and discharges its stored deposit atomically before replacement
KEY_AUTHORIZE(Renew) the named generation's issuing epoch is current, neither its expiry nor any validity-window end has been reached, and the resulting expiry extends the old one sets expires_at_ms = max(old_expires_at_ms, block_timestamp_ms + key_term_ms) with checked arithmetic; preserves the generation, replay mark, class, scope, validity window, verification policy, and deposit metadata; pays the ordinary byte and work charge, and neither a creation fee nor a new deposit
KEY_REVOKE(One(key_id, expected_generation)) the named generation is resident, whether currently usable or already invalidated deletes the raw record and discharges its deposit
KEY_REVOKE(All(expected_delegation_epoch)) key_allocation_counter > epoch_start_key_counter, so a key has been authorized in the current epoch increments the delegation epoch with checked arithmetic, sets epoch_start_key_counter to the current value of key_allocation_counter, and emits one domain event carrying the old and new epochs (section 14)

R-7.32. Every one of these variants is root-authorized and is bound to the expected keychain_revision.

R-7.33. KEY_REVOKE(All) invalidates every earlier delegated key immediately, including SOCIAL keys, without enumerating or deleting records, and performs constant work independent of the key count.

R-7.34. KEY_REVOKE(All) leaves the invalidated keys' replay marks and locked deposits untouched, and a later RECLAIM or reauthorization discharges each deposit exactly once (section 9).

R-7.35. Both KEY_REVOKE variants carry no command fee and remain fully resource-metered (section 10).

R-7.36. The delegation epoch, key_allocation_counter, and epoch_start_key_counter all start at 1, every authorization consumes and advances key_allocation_counter, and individual deletion never changes epoch_start_key_counter.

R-7.37. Every successful authorize, renew, or revoke advances keychain_revision, and none of them resets the key allocation counter or the root replay mark.

R-7.38. Cleanup by RECLAIM does not advance keychain_revision.

R-7.39. Revocation is forward-only: it never invalidates a finalized command, a revoked key never returns to active, and a re-added public key gets a new generation.

Renewal extends a live generation and never restores lost authority. Because a bulk revocation requires a paid authorization in the current epoch and then moves the watermark, one paid authorization enables at most one uncharged bulk revocation, and repeated empty revocations cannot create unlimited free work.

INV-AUTH-03. Revocation is forward-only; a re-added key gets a new generation.

INV-AUTH-07. A delegated key grants authority only under its issuing delegation epoch, and bulk revocation never resets a replay mark or changes a finalized command.

INV-AUTH-08. A project-scoped key authorizes only that project's pushes and scoped social messages, always subject to the applicable account role, capability, and lifecycle checks.

INV-AUTH-09. Renewal never restores an expired, revoked, or epoch-invalidated key generation.

7.4 Signing

Every family signs one protocol digest, the intent_digest of the command's CommandIntent:

intent_digest = BLAKE3("makechain.command" || canonical(CommandIntent))

CommandIntent binds these fields:

Field What it binds Present in
chain_id the chain the command may execute on every intent
envelope version the envelope format version every intent
ruleset_id the semantics under which the command executes every intent
price_schedule_id the prices and work weights applied (section 10) every intent
authority context, tagged which authority branch signs every intent
command payload, tagged the command tag and its payload every intent
actor the account the command acts for account context
key_id the signing slot account context
authorization_generation the generation of that slot account context
sequence the replay position (section 7.6) account context
max_charge the ceiling on the command's total debit account context
valid_before_timestamp_ms an optional stale-intent bound account context
governance revision and nonce the governance schedule position (section 18) governance context

A permissionless settlement intent has no account authorization context. The three contexts are explicit union branches, never zero-account or zero-sequence sentinels. Canonical field order, the allowed authority, authorization, and evidence combinations, and the digest preimage are fixed by encoding section 3.1 and encoding section 11.

R-7.40. A command is valid only if its ruleset_id equals the active ruleset at the height it executes, so a command signed under one set of semantics never executes under another.

R-7.41. Retired; its content is R-10.5.

A non-parameter release therefore invalidates every pending unfinalized command at activation, and clients re-sign. A parameter release preserves the ruleset, and a changed price schedule still invalidates every intent bound to its predecessor schedule (sections 10 and 18).

Root authority presents its signatures as one authorization node:

RootAuthorizationNodeV1 = SingleKey { key, signature }
                        | Configured { config, approvals[1..8] }
node_digest = BLAKE3("makechain.root-authorization" || intent_digest || A || u64(version))

The node digest for account A at configuration version binds the exact intent, the account the node speaks for, and that account's current version; it is the binding of TIP-1114's multisig_digest expressed in BLAKE3. The single-key branch presents the secp256k1 genesis key and its signature over that same account and version-zero node digest, and a configured genesis account supplies the configuration from which its address derives.

R-7.42. The single-key branch is valid only when the account's commitment is unset and its version is zero.

R-7.43. The node's config MUST be checked against the commitment in the account's AccountRoot in the state the command executes against, and never against a value the witness supplies.

R-7.44. A configured root witness is valid only if all of the following hold:

a. every approval is by a key that is an owner listed in the configuration and signs node_digest through its family wrapper, an approval by an owner account being invalid; b. approvals appear in owner order; c. each owner appears at most once; d. the summed weight of every approval before the last is below the threshold; e. the summed weight of all approvals is at least the threshold, so the last approval is the one that crosses it; f. no approval follows the one that crosses the threshold.

R-7.45. An owner WebAuthn signature MUST carry an rpIdHash and an origin from the chain's governed root-origin allowlist (section 18), because an owner has no KEY_AUTHORIZE at which to pin them and the witness MUST NOT supply its own policy.

R-7.46. A secp256k1 owner signs EIP-712 typed data derived from the decoded canonical intent, carrying command-specific typed fields, namely recipient, new owners and threshold, project, ref name and target, key scope, revocation variant, delegation epoch, and amount, plus account, configVersion, and intent_digest.

R-7.47. In that typed data domain.chainId is the Tempo host chain id, so that browser wallets sign, and Makechain's full chain_id is the domain salt.

R-7.48. The verifier decodes the intent, rebuilds the struct, and requires every displayed field to match, and there is no digest-only owner variant.

R-7.49. A root witness carries from 1 to 8 approvals, both inclusive, which is the structural cap of the approvals field, and every approval is charged through the work-weight table by family (section 10).

R-7.50. The remaining families sign as follows:

a. an Ed25519 key signs intent_digest under SSHSIG framing with namespace makechain.command:<chain_id_hex>; b. a P256 direct key signs SHA-256("makechain.p256" || intent_digest); c. a WebAuthn challenge is intent_digest for a delegated key and node_digest for an owner.

Each chosen approval set has one structural encoding, but different sufficient signer subsets may authorize the same intent and yield different envelopes, so intent-level deduplication never assumes a unique root witness. The complete signed envelope is identified by command_id = BLAKE3("makechain.command-envelope" || canonical(SignedCommand)), which is not stable for one intent, because WebAuthn client data varies per signature and unsigned evidence may be replaced (section 8).

R-7.51. Because WebAuthn client data varies per signature, admission dedup keys on (key_id, generation, sequence, intent_digest), and never on command_id (see ../docs/operations.md).

INV-AUTH-06. A root witness is valid only for the account's current authority version, reaches the threshold exactly at its last signature, and contains no signature beyond it; a witness under a superseded version is invalid.

INV-CONS-06. A signature for one command binds tag, version, ruleset, price schedule, actor, and authorization context, and can never be lifted into another command or executed under another ruleset.

7.5 Root configuration

ROOT_CONFIGURE replaces an account's owner set and threshold without changing its identity. The same command performs the in-place upgrade of a single-key account to a configured one, which is Tempo's in-place migration (TIP-1113) applied through Makechain's own authority state.

R-7.52. ROOT_CONFIGURE is authorized by the account's current quorum, installs a new owner set and threshold, increments version by exactly one with checked arithmetic, and stores the new commitment, while AccountId stays the genesis address.

R-7.53. The first ROOT_CONFIGURE of a single-key account is valid only with an unset commitment and the genesis witness, which is the account's key, and does not additionally require an otherwise unused account.

R-7.54. That first configuration moves version from 0 to 1 and sets salt = keccak256("tempo:multisig:upgrade" || AccountId).

R-7.55. Every later update preserves the current configuration's salt, and a configured genesis account preserves its initial salt.

R-7.56. Reconfiguration never clears AccountRoot metadata and never resets root_sequence, the delegation epoch, key_allocation_counter, epoch_start_key_counter, or keychain_revision.

R-7.57. Reconfiguration preserves the root signing slot and advances its existing mark normally, while changing the version required of subsequent commands.

R-7.58. Reconfiguration never restores the genesis-key authorization path, which is Tempo's permanent root-key disablement (TIP-1112), and the original key keeps authority only if the new configuration lists its address as an owner.

R-7.59. Delegated keys retain their existing validity or invalidity across reconfiguration, and bulk invalidation is the explicit KEY_REVOKE(All) variant and never a side effect of changing owners.

R-7.60. A keychain command signed against a superseded configuration version is invalid.

Loss of one owner is recovered by the remaining quorum reconfiguring; loss of the quorum itself is permanent (section 4). The following Tempo mechanisms are not adopted, because Makechain has its own delegated keychain and ROOT_CONFIGURE and holds no assets on other chains: access keys (TIP-1111), the configuration precompile (TIP-1109), and the cross-chain recovery wallet (TIP-1110).

7.6 Replay and authority context

Replay protection is a per-key high-water mark, not a global nonce, and there is no expiring seen-set. A command additionally binds the authority context it was signed against, so that a revoked and re-granted authority never revives an older intent.

R-7.61. Every account-signed command carries a sequence, and state keeps a high-water mark per (key_id, authorization_generation).

R-7.62. A command is valid only if its sequence strictly exceeds that mark.

R-7.63. Within a block the sequences of one key MUST be strictly ascending, while gaps are allowed and several pending sequences per key may coexist.

R-7.64. valid_before_timestamp_ms is a complementary stale-intent bound, is exclusive at execution time, and is never the replay mechanism.

A sequence is not a delivery guarantee: a higher sequence can finalize before an earlier command arrives, permanently superseding it. Object-level compare-and-set values, namely ref revisions, role generations, offer revisions, username revisions, and the keychain revision, govern concurrency and not replay (sections 8 and 9).

A project's control_generation is a counter that an ownership transfer rotates, and a role's role_grant_generation is the generation its grant received (section 8).

R-7.65. Every project-authorized intent binds the project's control_generation and the authorizing role_grant_generation, except that the controller case binds control_generation and omits role_grant_generation.

R-7.66. An intent that expects the absence of a child record binds the parent's mutation counter.

R-7.67. A command whose bound context no longer matches state is invalid, so a re-granted role or a transferred-and-returned project never revives an older signed intent.

R-7.68. Command-level authority and parent-counter guards are checked against the overlay at command entry, before any allocation is consumed, and ref-operation compare-and-set then follows sequential operation order inside the command, so the command's own writes do not invalidate its entry context.

R-7.69. Retired; its content is R-8.39.

R-7.70. Retired; its content is R-8.60.

An unrelated mutation can advance a shared parent counter and invalidate a pending create; a client then obtains a new signature against refreshed state rather than weakening the absence guard (see ../docs/client-api.md). Physical deletion by RECLAIM removes no live authority, and does discharge the resident deposit (section 9).

INV-AUTH-02. A command is valid only if its sequence strictly exceeds the mark for (key_id, generation); there is no global nonce.

INV-AUTH-04. A role change or control transfer can never revive a command signed under an older generation.

7.7 Rationale

This subsection is informative.

A weighted quorum root removes the single failure a one-key root cannot survive, which is loss or compromise of that key before any rotation. Adopting the settlement chain's account derivation makes one configuration one identity on both chains, so credits deposited to an account's own address are controlled by the same owners. Authority is replicated rather than proven from the settlement chain: a proof-based model exists only to manage a lag window after a key change there, couples every account action to that chain's liveness and to a relayer, and leaves validators a stale-proof problem that can be bounded but not removed. In the replicated model the owners close that window themselves in one block. A per-key high-water sequence is preferred over a bounded seen-set because a deterministic mark needs no eviction policy. Further rationale, including the signing-profile and origin-allowlist alternatives, is in ../docs/architecture.md.