Tutorial · Developers · Testnet

Prove You Own an Address: Sign and Verify a Message

Sign a short text with the key behind one of your addresses, hand the signature to anyone, and let them check it against the address. No coins move and the private key never leaves your wallet.

About 15 minutes · Checked 2026-10-21

1What a signed message proves

A Divi address is the hash of a public key, and only the holder of the matching private key can produce a signature that fits it. So a signature over the text "I control this address, 21 October 2026" proves exactly that, to anyone who has the address, the text and the signature. Nothing is sent to the network and nothing is spent.

Sign with
signmessage
Check with
verifymessage
Signature
65 bytes, base64

Typical uses: an exchange or support desk asks you to prove an address is yours; you publish a statement that only the address holder could have written; a website lets you log in by signing a one-time challenge (how a page asks the extension to sign is in The Browser Extension API). Both Divi nodes sign and verify the same way, and a signature from one verifies on the other.

Sign only text you have read

A signature is proof you agreed to the exact text. Never sign a blob someone else pasted in. And the private key itself is never part of this: anyone who asks for the key, a recovery phrase or a passphrase instead of a signature is not verifying anything.

2Sign a message

You need a wallet that holds the address. Signing does not touch the chain, so the node need not be synced. Everything below was run on testnet, where addresses start with x or y; the commands are the same on mainnet.

With IronDivi

irondivi-cli --testnet signmessage xxwaTGZFoB48cm6MYvvhfpXWP2mCwMHoXT "hello from IronDivi"
IEV2zP/iu6EvMEOLHy/Xs7ioT4pBFDgK4KJ1pcpRDehjQcPeDNsdqeWsT8cjio0PWa+bzE8N9pB2UtL9EtsTt8M=

The answer is the signature, one line of base64. Keep the address, the exact text and the signature together: that is the whole proof. If the wallet is encrypted, unlock it first with walletpassphrase "your passphrase" 60, or signing fails with Wallet is locked.

IronDivi takes exactly two arguments

IronDivi's call is signmessage "address" "message". It signs the text as typed; a third argument such as hex is ignored (see section 6). help signmessage answers Method not found: signmessage in IronDivi 0.2.6. That only means the help entry is missing; the command itself works.

With the Divi node (divi-cli)

./divi-cli signmessage xybfC8XGp89W5TQCfk8dBLjDvbWdE81DNe "hello from divid"
H3AwHLpo7upVce92JjZYq3Xq1++ErqOSROfBwzcHUKlnLFLM8ytayj13IeiadlFowfHINM3nQLZy9iYwr2d5JH0=

Same shape, same kind of answer. The C++ node accepts two more optional arguments, input_format and output_format, each either empty or hex (the input format also accepts an undocumented b64):

# message given as hex bytes (68656c6c6f is "hello"): same signature as signing "hello"
./divi-cli signmessage xybfC8XGp89W5TQCfk8dBLjDvbWdE81DNe 68656c6c6f hex
ILGapoS3bs9WI6aGo2fCQDpjCth5GxTvyu3ZrOLAHs+sHnDrW/e3wYKIPBehXwE4PWLg4TWN9Wjca7eH6BRv6fQ=

# signature returned as hex instead of base64
./divi-cli signmessage xybfC8XGp89W5TQCfk8dBLjDvbWdE81DNe hello "" hex
20b19aa684b76ecf5623a686a367c2403a630ad8791b14efcaedd9ace2c01ecfac1e70eb5bf7b7c182883c17a15f01383d62e0e1358df568dc6bb787e8146fe9f4

The hex input form is handy when the message holds bytes that are awkward on a command line. Most people can ignore both options: verifymessage only takes base64, so hand out the base64 form.

3Verify a signature

Verification needs no wallet and no key, only a node. It answers true or false. Here the IronDivi signature from step 2 is checked on the C++ node, and the C++ signature on IronDivi:

./divi-cli verifymessage xxwaTGZFoB48cm6MYvvhfpXWP2mCwMHoXT "IEV2zP/iu6EvMEOLHy/Xs7ioT4pBFDgK4KJ1pcpRDehjQcPeDNsdqeWsT8cjio0PWa+bzE8N9pB2UtL9EtsTt8M=" "hello from IronDivi"
true

irondivi-cli --testnet verifymessage xybfC8XGp89W5TQCfk8dBLjDvbWdE81DNe "H3AwHLpo7upVce92JjZYq3Xq1++ErqOSROfBwzcHUKlnLFLM8ytayj13IeiadlFowfHINM3nQLZy9iYwr2d5JH0=" "hello from divid"
true

Change one character of the message and the same signature fails. That is the point: the signature binds the text.

./divi-cli verifymessage xxwaTGZFoB48cm6MYvvhfpXWP2mCwMHoXT "IEV2zP/iu6EvMEOLHy/Xs7ioT4pBFDgK4KJ1pcpRDehjQcPeDNsdqeWsT8cjio0PWa+bzE8N9pB2UtL9EtsTt8M=" "hello from IronDivi!"
false
What a true answer means

Whoever produced the signature held the private key of that address and signed exactly that text. It does not say when (put a date in the text) and it does not say the address holds any coins (look it up in the explorer). A multisig address (testnet 8…/9…) cannot sign messages at all: there is no single key behind it.

4Verify without a node: the public RPC

verifymessage is on the allowlist of the public testnet endpoint at https://services.divi.domains/api/testnet/rpc/, so a signature can be checked from any machine with curl. Signing is not on its allowlist.

curl -s -X POST https://services.divi.domains/api/testnet/rpc/ \
  -H "Content-Type: application/json" \
  -d '{"method":"verifymessage","params":["xxwaTGZFoB48cm6MYvvhfpXWP2mCwMHoXT","IEV2zP/iu6EvMEOLHy/Xs7ioT4pBFDgK4KJ1pcpRDehjQcPeDNsdqeWsT8cjio0PWa+bzE8N9pB2UtL9EtsTt8M=","hello from IronDivi"]}'
{"error":null,"id":null,"result":true}

# signing is refused
curl -s -X POST https://services.divi.domains/api/testnet/rpc/ -H "Content-Type: application/json" \
  -d '{"method":"signmessage","params":["xxwaTGZFoB48cm6MYvvhfpXWP2mCwMHoXT","x"]}'
{"result":null,"error":{"code":-32601,"message":"Method not allowed: signmessage"},"id":null}

How the endpoint behaves (no login, HTTP 200 on every answer, errors inside the JSON) is covered in Talk to a Divi node from Python and JavaScript.

5Verify from code

A site that asks users to prove an address only needs this. Standard library only. Point it at your own node instead if you do not want to depend on the public one.

import json, urllib.request

RPC = "https://services.divi.domains/api/testnet/rpc/"

def verify(address: str, signature: str, message: str) -> bool:
    body = json.dumps({"method": "verifymessage",
                       "params": [address, signature, message]}).encode()
    req = urllib.request.Request(RPC, body, {"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=15) as r:
        reply = json.load(r)
    if reply.get("error"):
        raise RuntimeError(reply["error"]["message"])   # e.g. Malformed base64 encoding
    return reply["result"] is True

print(verify("xxwaTGZFoB48cm6MYvvhfpXWP2mCwMHoXT",
             "IEV2zP/iu6EvMEOLHy/Xs7ioT4pBFDgK4KJ1pcpRDehjQcPeDNsdqeWsT8cjio0PWa+bzE8N9pB2UtL9EtsTt8M=",
             "hello from IronDivi"))
True

For a login flow, make the message a one-time challenge you generated (random text plus your site name and a timestamp), store it server side, and accept the signature only once and only within a few minutes. Otherwise an old signature can be replayed.

6What is inside a signature

Both nodes hash the same bytes: the fixed prefix "DarkNet Signed Message:\n" (a leftover of Divi's ancestry; Bitcoin uses "Bitcoin Signed Message:\n"), then the message, each with its length in front, hashed twice with SHA-256. The key signs that hash, and the 65-byte result (a recovery byte plus r and s) is base64-encoded. verifymessage recovers the public key from the signature, hashes it, and compares with the address.

IronDivi 0.2.6Divi node v3.0.0
Signsignmessage "address" "message"signmessage "address" "message" "input_format" "output_format"
Verifyverifymessage "address" "signature" "message", base64 signature only
Message prefixDarkNet Signed Message:\n
Signatures cross-verifyYes, both directions (step 3)
Hex in / hex outNo. signmessage A 68656c6c6f hex signs the literal text 68656c6c6fYes, with hex as the 3rd or 4th argument
Locked walletSigning fails with code -13 until walletpassphrase is run; verifying never needs the wallet

Because of the prefix, a Divi signature will not verify in a Bitcoin tool (and the other way round) even though the maths is the same. Use a Divi node, or the public RPC above.

If something goes wrong

The node saysWhat it means
{"code":-4,"message":"Private key not available"} (Divi node)
{"code":-5,"message":"Private key for address not found in wallet"} (IronDivi)
You asked to sign with an address this wallet does not hold the key for. Check that validateaddress says "ismine": true, or use the wallet that made the address.
{"code":-5,"message":"Malformed base64 encoding"} (Divi node, public RPC)
{"code":-5,"message":"Invalid base64 signature"} (IronDivi)
The signature is not base64: it was cut, had a space inserted, or is the hex form. Copy it again, whole, and quote it on the command line (it contains /, + and =).
{"code":-3,"message":"Invalid address"} (Divi node, and IronDivi verifymessage)
{"code":-5,"message":"Invalid address"} (IronDivi signmessage)
The address does not parse, or belongs to the other network. A testnet node wants an x/y address, a mainnet node a D address.
{"code":-3,"message":"Address does not refer to key"} (Divi node)The address is a multisig/script address. Those cannot sign messages. IronDivi's verifymessage simply answers false for such an address.
{"code":-13,"message":"Wallet is locked"} (IronDivi)
{"code":-13,"message":"Error: Please enter the wallet passphrase with walletpassphrase first."} (Divi node)
Run walletpassphrase "passphrase" 60 and sign within 60 seconds.
Method not allowed: signmessage (public RPC)The public endpoint cannot sign for you. Sign in your own wallet; verify anywhere.
verifymessage answers false although the signature is rightThe message differs by even one character, a trailing newline or a smart quote. Compare byte for byte; on the C++ node sign with the hex input form to be sure what was signed.

Next: use a signed challenge from a web page in The Browser Extension API, or build and sign a whole transaction by hand in Raw Transactions and Multisig. No testnet coins yet? Get testnet DIVI.

Still stuck? Ask the Divi chatbot, or see all tutorials.