# Ming Ke Ming (名可名) -- Account Module (Python)

[![License](https://img.shields.io/github/license/dimchat/mkm-py)](https://github.com/dimchat/mkm-py/blob/master/LICENSE)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/dimchat/mkm-py/pulls)
[![Platform](https://img.shields.io/badge/Platform-Python%203-brightgreen.svg)](https://github.com/dimchat/mkm-py/wiki)
[![Issues](https://img.shields.io/github/issues/dimchat/mkm-py)](https://github.com/dimchat/mkm-py/issues)
[![Repo Size](https://img.shields.io/github/repo-size/dimchat/mkm-py)](https://github.com/dimchat/mkm-py/archive/refs/heads/main.zip)
[![Tags](https://img.shields.io/github/tag/dimchat/mkm-py)](https://github.com/dimchat/mkm-py/tags)
[![Version](https://img.shields.io/pypi/v/mkm)](https://pypi.org/project/mkm)

[![Watchers](https://img.shields.io/github/watchers/dimchat/mkm-py)](https://github.com/dimchat/mkm-py/watchers)
[![Forks](https://img.shields.io/github/forks/dimchat/mkm-py)](https://github.com/dimchat/mkm-py/forks)
[![Stars](https://img.shields.io/github/stars/dimchat/mkm-py)](https://github.com/dimchat/mkm-py/stargazers)
[![Followers](https://img.shields.io/github/followers/dimchat)](https://github.com/orgs/dimchat/followers)

This [document](https://github.com/moky/DIMP/blob/master/MingKeMing-Identity.md) introduces a common **Account Module** for decentralized user identity authentication.

## Meta

The **Meta** was generated by your **private key**, it can be used to build a new ID for entity, or verify the ID/PK pair.

It consists of 4 fields:

| Field       | Description                              |
| ----------- | ---------------------------------------- |
| type        | Algorithm Version                        |
| key         | Public Key                               |
| seed        | Entity Name (Optional)                   |
| fingerprint | Signature to generate address (Optional) |

If ```seed``` exists, ```fingerprint = private_key.sign(seed)```

### Meta Type

1. ```MKM``` _(Default)_
2. ```BTC```
3. ~~Extended BTC~~
4. ```ETH```
5. ~~Extended ETH~~
6. ...

### Meta Example
```javascript
/* Meta(JsON) for hulk@4YeVEN3aUnvC1DNUufCq1bs9zoBSJTzVEj */
{
    "type"        : 0x01,
    "key"         : {
        "algorithm" : "RSA",
        "data"      : "-----BEGIN PUBLIC KEY-----\nMIGJAoGBALB+vbUK48UU9rjlgnohQowME+3JtTb2hLPqtatVOW364/EKFq0/PSdnZVE9V2Zq+pbX7dj3nCS4pWnYf40ELH8wuDm0Tc4jQ70v4LgAcdy3JGTnWUGiCsY+0Z8kNzRkm3FJid592FL7ryzfvIzB9bjg8U2JqlyCVAyUYEnKv4lDAgMBAAE=\n-----END PUBLIC KEY-----",
        "mode"      : "ECB",
        "padding"   : "PKCS1",
        "digest"    : "SHA256"
    },
    "seed"        : "hulk",
    "fingerprint" : "jIPGWpWSbR/DQH6ol3t9DSFkYroVHQDvtbJErmFztMUP2DgRrRSNWuoKY5Y26qL38wfXJQXjYiWqNWKQmQe/gK8M8NkU7lRwm+2nh9wSBYV6Q4WXsCboKbnM0+HVn9Vdfp21hMMGrxTX1pBPRbi0567ZjNQC8ffdW2WvQSoec2I="
}
```

## ID
The **ID** is used to identify an **entity**(user/group). It consists of 3 fields:

| Field       | Description                    |
| ----------- | ------------------------------ |
| type        | Entity type                    |
| name        | Same with meta.seed (Optional) |
| address     | Unique Identification          |
| terminal    | Login point (Optional)         |

The ID format is ```name@address[/terminal]```.

```
# ID examples
ID1 = "hulk@4YeVEN3aUnvC1DNUufCq1bs9zoBSJTzVEj";  // Immortal Hulk
ID2 = "moki@4WDfe3zZ4T7opFSi3iDAKiuTnUHjxmXekk";  // Monkey King
```

### ID Name
The **Name** field is a username, or just a random string for group:

1. The length of name must more than 1 byte, less than 32 bytes;
2. It should be composed by a-z, A-Z, 0-9, or charactors '_', '-', '.';
3. It cannot contain key charactors('@', '/').

```
# Name examples
user_name  = "Albert.Moky";
group_name = "Group-9527";
```

It's equivalent to ```meta.seed```

### ID Address

The **Address** field was created with the Meta and a **Network ID**:

#### BTC Address
```python
from typing import Optional

from mkm.types import ConstantString
from mkm.digest import sha256, ripemd160
from mkm.format import base58_encode, base58_decode
from mkm import Address


class BTCAddress(ConstantString, Address):
    """
        Address like BitCoin
        ~~~~~~~~~~~~~~~~~~~~

        data format: "network+digest+code"
            network    --  1 byte
            digest     -- 20 bytes
            check code --  4 bytes

        algorithm:
            fingerprint = PK.data
            digest      = ripemd160(sha256(fingerprint));
            code        = sha256(sha256(network + digest)).prefix(4);
            address     = base58_encode(network + digest + code);
    """

    def __init__(self, address: str, network: int):
        super().__init__(string=address)
        self.__type = network

    @property  # Override
    def network(self) -> int:
        return self.__type

    #
    #   Factory methods
    #
    @classmethod
    def from_data(cls, fingerprint: bytes, network: int) -> Address:
        """
        Generate address with fingerprint and network ID

        :param fingerprint: meta.fingerprint or key.data
        :param network:     address type
        :return: Address object
        """
        # 1. digest = ripemd160(sha256(fingerprint))
        digest = ripemd160(sha256(fingerprint))
        # 2. head = network + digest
        head = chr(network).encode('latin1') + digest
        # 3. cc = sha256(sha256(head)).prefix(4)
        code = check_code(head)
        # 4. data = base58_encode(head + cc)
        address = base58_encode(head + code)
        return cls(address=address, network=network)

    @classmethod
    def from_str(cls, address: str) -> Optional[Address]:
        """
        Parse a string for BTC address

        :param address: address string
        :return: Address object
        """
        if len(address) < 26 or len(address) > 35:
            return None
        # decode
        data = base58_decode(address)
        if data is None or len(data) != 25:
            return None
        # check code
        prefix = data[:21]
        suffix = data[21:]
        if check_code(prefix) == suffix:
            network = ord(data[:1])
            return cls(address=address, network=network)


def check_code(data: bytes) -> bytes:
    # check code in BTC address
    return sha256(sha256(data))[:4]
```

#### ETH Address
```python
from typing import Optional

from mkm.types import ConstantString
from mkm.digest import keccak256
from mkm.format import hex_encode
from mkm import Address, EntityType

class ETHAddress(ConstantString, Address):
    """
        Address like Ethereum
        ~~~~~~~~~~~~~~~~~~~~~

        data format: "0x{address}"

        algorithm:
            fingerprint = PK.data
            digest      = keccak256(fingerprint)
            address     = hex_encode(digest.suffix(20))
    """

    def __init__(self, address: str):
        super().__init__(string=address)

    @property  # Override
    def network(self) -> int:
        return EntityType.USER.value

    @classmethod
    def validate_address(cls, address: str) -> Optional[str]:
        if is_eth(address=address):
            lower = address[2:].lower()
            return '0x%s' % eip55(address=lower)
        # not an ETH address

    @classmethod
    def is_validate(cls, address: str) -> bool:
        validate = cls.validate_address(address=address)
        return validate is not None and validate == address

    #
    #   Factory methods
    #
    @classmethod
    def from_data(cls, fingerprint: bytes) -> Address:
        """
        Generate ETH address with key.data

        :param fingerprint: key.data
        :return: Address object
        """
        if len(fingerprint) == 65:
            # skip first char
            fingerprint = fingerprint[1:]
        assert len(fingerprint) == 64, 'key data length error: %d' % len(fingerprint)
        # 1. digest = keccak256(fingerprint)
        digest = keccak256(data=fingerprint)
        # 2. address = hex_encode(digest.suffix(20))
        tail = digest[-20:]
        address = '0x' + eip55(address=hex_encode(data=tail))
        return cls(address=address)

    @classmethod
    def from_str(cls, address: str) -> Optional[Address]:
        """
        Parse a string for ETH address

        :param address: address string
        :return: Address object
        """
        if is_eth(address=address):
            return cls(address=address)


# https://eips.ethereum.org/EIPS/eip-55
def eip55(address: str) -> str:
    res = ''
    table = keccak256(address.encode('utf-8'))
    for i in range(40):
        ch = address[i]
        x = ord(ch)
        if x > 0x39:
            # check for each 4 bits in the hash table
            # if the first bit is '1',
            #     change the character to uppercase
            x -= (table[i >> 1] << (i << 2 & 4) & 0x80) >> 2
            ch = chr(x)
        res += ch
    return res


def is_eth(address: str) -> bool:
    if len(address) != 42:
        return False
    if address[0] != '0' or address[1] != 'x':
        return False
    for i in range(2, 42):
        ch = address[i]
        if '0' <= ch <= '9':
            continue
        if 'A' <= ch <= 'Z':
            continue
        if 'a' <= ch <= 'z':
            continue
        # unexpected character
        return False
    return True
```

When you get a meta for the entity ID from the network,
you must verify it with the consensus algorithm before accepting its **public key**.

(All data encode with **BASE64** algorithm as default, excepts the **address**)

----

Copyright &copy; 2018-2025 Albert Moky
[![Followers](https://img.shields.io/github/followers/moky)](https://github.com/moky?tab=followers)
