Is this Grin Wallet URL Reachable?

Awesome!

Well, would you mind to share your code? I could try to fix it and then explain it here:

If you’re familiar with python here’s another testing code:

"""
Key Generation
Keys used in SlatepackAddresses are derived from a path from the master seed in a given wallet account. Currently the wallet uses separate derivation paths: one for the private bytes used for the blinding factor keys and one for the private bytes used to derive the ed25519 keys used to derive Tor onion addresses. ed25519 keys used for a SlatepackAddress are derived from this second derivation path of the master seed.

SlatepackAddress keys may be derived in parallel to the blinding factor derivation path such that a unique SlatepackAddress is derived each time a new blinding factor is derived for a transaction to satisfy the requirement for a unique SlatepackAddress to be used for each transaction by default.

In a future update it may be desirable to encode the derivation path for the SlatepackAddress for a given encrypted SlatepackMessage somewhere so that the x25519 decryption keys can be derived without grinding down the path to find the right key to use.

Example SlatepackAddress
grin1p4fuklglxqsgg602hu4c4jl4aunu5tynyf4lkg96ezh3jefzpy6swshp5x
"""

import base64
import binascii
from hashlib import sha3_256

import nacl.encoding
import nacl.hash
import nacl.signing
from Cryptodome.Random import get_random_bytes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from ecdsa import SigningKey, ecdsa
from mnemonic import Mnemonic

import segwit_addr
from seed import decrypt, encrypt


def generate_seed(length):
    return get_random_bytes(int(((4 * length) / 3)))


def tor_address(key_bytes):
    version = b"\x03"
    checksum = sha3_256(b".onion checksum" + key_bytes + version).digest()
    return base64.b32encode(key_bytes + checksum[0:2] + version).lower()


def encode_grin1_address(vk, hrp="grin"):
    values = segwit_addr.convertbits(list(vk), 8, 5)
    checksum = segwit_addr.bech32_create_checksum(
        hrp, values, segwit_addr.Encoding.BECH32
    )
    combined = values + checksum
    return hrp + "1" + "".join([segwit_addr.CHARSET[d] for d in combined])


def decode_grin1_address(addr, hrp="grin"):
    data = segwit_addr.bech32_decode(addr)
    decoded = segwit_addr.convertbits(data[1], 5, 8, False)
    return bytes(decoded)


if __name__ == "__main__":
    key_pair = Ed25519PrivateKey.from_private_bytes(
        bytes.fromhex(
            "29a5b01c3ecf2dff63e30d8857f12a2bc99e0ab51a610c5c33f5035070d62a0b"
        )
    )
    public_key = key_pair.public_key().public_bytes(
        encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
    )
    address = tor_address(public_key).decode("utf8")
    grin1 = encode_grin1_address(public_key)
    print(f"Public Key:\t {public_key.hex()}")
    print(f"Tor Address:\t {address}")
    print(f"Bech32 Address:\t {grin1}")

    assert (
        public_key.hex()
        == "dcb57a361f64ca43b6c82fde6b2c771f0408d5115c007b90b1f249bc4efc5fcc"
    )
    assert address == "3s2xunq7mtfehnwif7pgwldxd4carvirlqahxefr6je3ytx4l7glq5yd"
    assert grin1 == "grin1mj6h5dslvn9y8dkg9l0xktrhruzq34g3tsq8hy937fymcnhutlxqkuf6xx"

    decoded = decode_grin1_address(grin1, "grin")
    assert decoded.hex() == public_key.hex()
    assert address == tor_address(public_key).decode("utf8")
    print(f"Decoded:\t {decoded.hex()}")

You can get the segwit_addr code here: https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py

3 Likes

Thanks for the link, much appreciated

Forum software didnot allow me to upload script file, you can find the code below.

const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
const CHARSET = ‘qpzry9x8gf2tvdw0s3jn54khce6mua7l’;
const CHARKEY_KEY = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1];

/**

  • Converts string to int array like php unpack function

  • @param {*} inp
    */
    function unpack(inp){

    ret =
    for(let i=0;i<inp.length;i++){
    ret.push(inp.charCodeAt(i));
    }
    return ret;
    }

function pack(inp_arr){

ret = "";
for (let i=0;i<inp_arr.length;i++){
    ret += String.fromCharCode(inp_arr[i]);
}

return ret;

}

function ord(c){
return c.charCodeAt(0);
}

function polyMod(values,numvalues){

let chk = 1;
for(let i =0; i<numvalues;i++){
    let top = chk >> 25;
    chk = (chk & 0x1ffffff) << 5 ^  values[i];

    for (let j=0;j<5; j++){
        let value = ((top >> j)& 1) ? GENERATOR[j] : 0
        chk ^= value;
    }
}

return chk;

}

/**

  • Expands the human readable part into a character array for checksumming.

  • @param string hrp

  • @param int hrpLen

  • @return int
    */
    function hrpExpand(hrp,hrpLen){
    let expand1 = ;
    let expand2 = ;

    for (let i=0;i< hrpLen;i++){
    let o = ord(hrp[i]);
    expand1.push(o >>5);
    expand2.push(o & 31);
    }

    return expand1.concat([0]).concat(expand2);
    }

/**

  • Converts words of $fromBits bits to $toBits bits in size.

  • @param int data - character array of data to convert

  • @param int inLen - number of elements in array

  • @param int fromBits - word (bit count) size of provided data

  • @param int toBits - requested word size (bit count)

  • @param bool pad - whether to pad (only when encoding)

  • @return int

  • @throws Exception
    */
    function convertBits(data,inLen,fromBits,toBits,pad=true){
    let acc =0;
    let bits = 0;
    let ret =
    let maxv = (1 << toBits) -1;
    let maxacc = ( 1 << (fromBits + toBits -1)) -1;

    for (let i = 0; i< inLen; i++){
    let value = data[i];
    if (value < 0 || value >> fromBits){
    throw new Error(‘Invalid value for convert bits’);
    }

     acc = ((acc << fromBits) | value ) & maxacc;
     bits += fromBits;
    
     while (bits >= toBits){
         bits -= toBits;
         ret.push( ((acc>>bits)& maxv) );
     }
    

    }

    if (pad)
    {
    if (bits){
    ret.push( (acc<< toBits - bits) &maxv );
    }
    } else if (bits >= fromBits || (((acc << (toBits - bits))) & maxv))
    {
    throw new Error(‘Invalid Data’);
    }

    return ret;
    }

/**

  • Verifies the checksum given hrp and convertedDataChars.

  • @param string hrp

  • @param int convertedDataChars

  • @return bool
    */
    function verifyChecksum(hrp, convertedDataChars)
    {
    expandHrp = hrpExpand(hrp, hrp.length );
    let r = expandHrp.concat(convertedDataChars);
    let poly = polyMod(r, r.length);

    return poly === 1;
    }

/**

  • @throws Exception

  • @param string sBech - the bech32 encoded string

  • @return array - returns [hrp, dataChars]
    */
    function decodeRaw(sBech){

    length = sBech.length;

    if(length<8) {
    throw new Error(“Bech32 string is too short”);
    }

    chars = unpack(sBech);

    let haveUpper=false;
    let haveLower=false;
    let positionOne = -1;

    for(let i=0;i<length;i++){

     let x = chars[i];
     console.log("x: ",x);
    
     if (x< 33 || x > 126)
     {
         throw new Exception("Out of range character in bech32 string");
     }
    
     if (x >= 0x61 && x <= 0x7a)
     {
         haveLower = true;
     }
    
     if (x >= 0x41 && x <= 0x5a)
     {
         haveUpper = true;
         x = chars[i] = x + 0x20;
     }
    
     // find location of last '1' character
     if (x === 0x31)
     {
         positionOne = i;
     }
    

    }

    if (haveUpper && haveLower)
    {
    throw new Error(‘Data contains mixture of higher/lower case characters’);
    }

    if (positionOne === - 1)
    {
    throw new Error(“Missing separator character”);
    }

    if (positionOne < 1)
    {
    throw new Error(“Empty HRP”);
    }

    if ((positionOne + 7) > length)
    {
    throw new Error(‘Too short checksum’);
    }

    let hrp = pack(chars.slice(0,positionOne));

    let data =

    for (let i=positionOne + 1;i<length;i++){

     data.push( (chars[i] & 0x80 ) ? -1 : CHARKEY_KEY[chars[i]]);
    

    }
    console.log("hrp: ",hrp.length, hrp);
    console.log("data: ",data.length,data);
    if (!verifyChecksum(hrp, data))
    {
    throw new Error(‘Invalid bech32 checksum’);
    }

    return [hrp, data.slice(0,-6) ];
    }

/**

  • Validates a bech32 string and returns [hrp, dataChars] if

  • the conversion was successful. An exception is thrown on invalid

  • data.

  • @param string sBech - the bech32 encoded string

  • @return array - returns [hrp, dataChars]

  • @throws Exception
    */
    function decode(sBech)
    {

    if (sBech.length > 90)
    {
    throw new Error(‘Bech32 string cannot exceed 90 characters in length’);
    }

    return decodeRaw($sBech);
    }

// BASE32

const ALPHABET = ‘ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=’;

/**

  • Maps the Base32 character to its corresponding bit value.
    */
    const MAPPING = {
    ‘=’ : 0b00000,
    ‘A’ : 0b00000,
    ‘B’ : 0b00001,
    ‘C’ : 0b00010,
    ‘D’ : 0b00011,
    ‘E’ : 0b00100,
    ‘F’ : 0b00101,
    ‘G’ : 0b00110,
    ‘H’ : 0b00111,
    ‘I’ : 0b01000,
    ‘J’ : 0b01001,
    ‘K’ : 0b01010,
    ‘L’ : 0b01011,
    ‘M’ : 0b01100,
    ‘N’ : 0b01101,
    ‘O’ : 0b01110,
    ‘P’ : 0b01111,
    ‘Q’ : 0b10000,
    ‘R’ : 0b10001,
    ‘S’ : 0b10010,
    ‘T’ : 0b10011,
    ‘U’ : 0b10100,
    ‘V’ : 0b10101,
    ‘W’ : 0b10110,
    ‘X’ : 0b10111,
    ‘Y’ : 0b11000,
    ‘Z’ : 0b11001,
    ‘2’ : 0b11010,
    ‘3’ : 0b11011,
    ‘4’ : 0b11100,
    ‘5’ : 0b11101,
    ‘6’ : 0b11110,
    ‘7’ : 0b11111,
    };

function Base32_encode(inp_str){

// Empty string results in empty string
if ('' === inp_str ) {
    return '';
}

let encoded='';

// set the initial values
let n = 0;
let bitLen = 0;
let val = 0;
let len = inp_str.length;

// pad the end of the string - this ensures that there are enough zeros
inp_str = inp_str.concat(String.fromCharCode(0).repeat(4));

// Explode string into integers
let chars = unpack(inp_str);

while (n < len || 0 != bitLen){
     //If the bit length has fallen below 5, shift left 8 and add the next character.
     if (bitLen < 5) {
        val = val << 8;
        bitLen += 8;
        n++;
        val += chars[n];
    }
    let shift = bitLen - 5;
    encoded += (n - ( bitLen > 8) > len && 0 == val) ? '=' : ALPHABET[val >> shift];
    val = val & ((1 << shift) - 1);
    bitLen -= 5;
}

return encoded;

}

/**

  • Decodes base32.
  • @param string base32String Base32 encoded string
  • @return string Clear text string
    */
    function Base32_decode(base32String){
base32String = base32String.toUpperCase();
base32String = base32String.replace(/[^A-Z2-7]/,'');

// Empty string results in empty string
if ('' === base32String || null === base32String) {
    return '';
}

let decoded = '';

 //Set the initial values
 let len = base32String.length;
 let n = 0;
 let bitLen = 5;
 let val = MAPPING[base32String[0]];

 while (n < len){

      //If the bit length has fallen below 8, shift left 5 and add the next pentet.
      if (bitLen < 8) {
        val = val << 5;
        bitLen += 5;
        n++;
        let pentet = '=';
        if (base32String[n]!==null){
            pentet = base32String[n];
        }            

        //If the new pentet is padding, make this the last iteration.
        if ('=' === pentet) {
            n = len;
        }

        val += MAPPING[pentet];
        continue;
    }
    
    let shift = bitLen - 8;

    decoded += String.fromCharCode($val >> $shift);
    val = val & ((1 << $shift) - 1);
    bitLen -= 8;

 }

 return decoded;

}

// BASE32 END

let wallet=“grin1zxwrf5yaxlyps4mpx3n7j9kp4su3gzgpdhfk2sgv56q0prcdlzls9e6e0y”;
let correct_address = “cgodjue5g7ebqv3bgrt6sfwbvq4ricibnxjwkqimu2apbdyn7c752iyd”;

let data = decodeRaw(wallet)[1];
let hrp_data = decodeRaw(wallet)[0];
console.log(“hrp_data”);
console.log(hrp_data);
let decoded = convertBits(data,data.length,5,8,false);

console.log(data);
console.log("\ndecoded (length: "+ decoded.length);
console.log(decoded);

console.log(“CORRECT THIS FAR ----------------------------------------\n\n”);
// Correct array [17,156,52,208]

let pkb = pack( decoded);
console.log("pkb(pack) : "+pkb.length.toString());
console.log(pkb);

const {createHash} = require(‘crypto’);

let checkSumArray = “.onion checksum” + pkb + “\x03”;

// Not sure which encoding to use, so I try each one of them
let encodings = [‘utf8’,‘binary’,‘hex’,‘ascii’,‘latin1’]

console.log('checksum: ');

for (let i =0;i<encodings.length;i++){
let hash = createHash(‘sha256’);

let checksumBuffer = Buffer.from(checkSumArray,encodings[i]);    
hash.update( checksumBuffer);
let checksum = hash.digest('base32');

console.log(checksum);

}

let wallet_address = Base32_encode(pkb);
console.log(“\n\nDerived address:”);
console.log(wallet_address);
console.log(“correct address:”);
console.log(correct_address);

console.log(“\n\n----- END -------”);

Based on your python example, I tried to pinpoint the point I failed.

Here is python code for checksum hash calculation. I omitted pkb for sake of brevity.

resim

Here is javascript code with output. Assuming python output is correct, I cannot get the same hex value with javascript.

    const {createHash} = require('crypto');

    let checkSumArray = ".onion checksum"  + "\x03";

    // Not sure which encoding to use, so I try each one of them

    let encodings = ['utf8','binary','hex','ascii','latin1']

    for (let i =0;i<encodings.length;i++){

        let hash = createHash('sha256');         

        let checksumBuffer = Buffer.from(checkSumArray,encodings[i]);    

        hash.update( checksumBuffer);

        let checksum = hash.digest('hex');        

        console.log(checksum);

}

a163815c8dbe4f209fce148c15d92998c579eb8eaa691cbf746ba7c87b08a9ca
a163815c8dbe4f209fce148c15d92998c579eb8eaa691cbf746ba7c87b08a9ca
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
a163815c8dbe4f209fce148c15d92998c579eb8eaa691cbf746ba7c87b08a9ca
a163815c8dbe4f209fce148c15d92998c579eb8eaa691cbf746ba7c87b08a9ca
  const ed = require('noble-ed25519')
  const base32 = require('hi-base32')
  const sha3_256 = require('js-sha3').sha3_256
  const { bech32 } = require('bech32')
  const grin1 = 'grin1mj6h5dslvn9y8dkg9l0xktrhruzq34g3tsq8hy937fymcnhutlxqkuf6xx'
  const chekcsumstr = Buffer.from('.onion checksum', 'utf8')
  const onion_version = Buffer.from('03', 'hex')

  function tor_address(public_key) {
    pubKey = Buffer.from(public_key, 'hex')
    var CHECKSUM = Buffer.from(sha3_256.create().update(Buffer.concat([chekcsumstr, pubKey, onion_version])).digest()).slice(0, 2)
    return base32.encode(Buffer.concat([pubKey, CHECKSUM, onion_version])).toLowerCase()
  }

  function encode_grin1_address(public_key, hrp = "grin") {
    const words = bech32.toWords(Buffer.from(public_key, 'hex'))
    return bech32.encode(hrp, words);
  }

  function decode_grin1_address(addr, hrp = "grin") {
    const { words } = bech32.decode(addr, hrp);
    const public_key = bech32.fromWords(words)
    return Buffer.from(public_key).toString('hex');
  }

  (async () => {
    const public_key = await ed.getPublicKey('29a5b01c3ecf2dff63e30d8857f12a2bc99e0ab51a610c5c33f5035070d62a0b')
    console.log(`Public Key:\t ${public_key}`);
    const address = tor_address(public_key)
    console.log(`Tor Address:\t ${address}`);
    console.log(`Public Key: ${decode_grin1_address(grin1)}`);
    console.log(`Bech32 Address: \t ${encode_grin1_address(public_key)}`)
  })()

code in nodejs , hope can help you

2 Likes

your python code is really nice. i have translated it into the nodejs code。
but how to get the the private key key from mnemonic。It makes me confused for a few days。
could you give an python example. really thanks。

2 Likes

Thanks so much @MrFox, I have integrated it into backend and it is working.

3 Likes

It’s useful. Why not make it a RESTFul API ? I’d like to integrate it into my web wallet project.

Hi @biganiseed
Wallet check service is available as part of grinnode.live public api

Cool! Thanks for the information.

You are welcome mate