Module: Mpp::Methods::Tempo::Proof

Defined in:
lib/mpp/methods/tempo/proof.rb

Overview

EIP-712 proof credentials for zero-amount challenges.

Domain: { name: “MPP”, version: “2”, chainId } Types: { Proof: [{ name: “challengeId”, type: “string” }, { name: “realm”, type: “string” }] } Message: { challengeId: <challenge.id>, realm: <challenge.realm> }

Constant Summary collapse

DOMAIN_NAME =
"MPP"
DOMAIN_VERSION =
"2"
DOMAIN_TYPE_HASH =

EIP-712 domain separator type hash

"EIP712Domain(string name,string version,uint256 chainId)"
PROOF_TYPE_HASH =
"Proof(string challengeId,string realm)"

Class Method Summary collapse

Class Method Details

.abi_encode(*values) ⇒ Object

ABI-encode values (packed 32-byte words).



98
99
100
101
102
103
104
105
106
107
# File 'lib/mpp/methods/tempo/proof.rb', line 98

def abi_encode(*values)
  values.map { |v|
    case v
    when String
      v.b.rjust(32, "\x00".b)
    when Integer
      uint256(v)
    end
  }.join
end

.domain_separator(chain_id) ⇒ Object

Compute the EIP-712 domain separator.



28
29
30
31
32
33
34
35
36
37
# File 'lib/mpp/methods/tempo/proof.rb', line 28

def domain_separator(chain_id)
  keccak256(
    abi_encode(
      keccak256(DOMAIN_TYPE_HASH),
      keccak256(DOMAIN_NAME),
      keccak256(DOMAIN_VERSION),
      uint256(chain_id)
    )
  )
end

.keccak256(data) ⇒ Object



22
23
24
25
# File 'lib/mpp/methods/tempo/proof.rb', line 22

def keccak256(data)
  Kernel.require "eth"
  Eth::Util.keccak256(data)
end

.parse_source(source_str) ⇒ Object

Parse a proof source DID. Returns { address:, chain_id: } or nil.



84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/mpp/methods/tempo/proof.rb', line 84

def parse_source(source_str)
  match = source_str.match(/\Adid:pkh:eip155:(0|[1-9]\d*):(.+)\z/)
  return nil unless match

  chain_id = Integer(match[1])
  address = match[2]
  return nil unless address.match?(/\A0x[a-fA-F0-9]{40}\z/)

  {address: address, chain_id: chain_id}
rescue ArgumentError
  nil
end

.recover_address(hash, sig_bytes) ⇒ Object



116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/mpp/methods/tempo/proof.rb', line 116

def recover_address(hash, sig_bytes)
  Kernel.require "eth"

  return nil unless sig_bytes.bytesize == 65

  sig_hex = "0x#{sig_bytes.unpack1("H*")}"
  # Use raw ecrecover (not personal_recover which adds EIP-191 prefix)
  recovered_key = Eth::Signature.recover(hash, sig_hex)
  Eth::Util.public_key_to_address(recovered_key).to_s
rescue => _e
  nil
end

.sign(account:, chain_id:, challenge_id:, realm:) ⇒ Object

Sign a proof credential (client-side).



58
59
60
61
62
# File 'lib/mpp/methods/tempo/proof.rb', line 58

def sign(account:, chain_id:, challenge_id:, realm:)
  hash = signing_hash(chain_id: chain_id, challenge_id: challenge_id, realm: realm)
  sig = .sign_hash(hash)
  "0x#{sig.unpack1("H*")}"
end

.signing_hash(chain_id:, challenge_id:, realm:) ⇒ Object

Compute the full EIP-712 signing hash.



51
52
53
54
55
# File 'lib/mpp/methods/tempo/proof.rb', line 51

def signing_hash(chain_id:, challenge_id:, realm:)
  keccak256(
    "\x19\x01".b + domain_separator(chain_id) + struct_hash(challenge_id, realm)
  )
end

.source(address:, chain_id:) ⇒ Object

Construct source DID for proof credentials.



79
80
81
# File 'lib/mpp/methods/tempo/proof.rb', line 79

def source(address:, chain_id:)
  "did:pkh:eip155:#{chain_id}:#{address}"
end

.struct_hash(challenge_id, realm) ⇒ Object

Compute the EIP-712 struct hash for Proof(challengeId, realm).



40
41
42
43
44
45
46
47
48
# File 'lib/mpp/methods/tempo/proof.rb', line 40

def struct_hash(challenge_id, realm)
  keccak256(
    abi_encode(
      keccak256(PROOF_TYPE_HASH),
      keccak256(challenge_id),
      keccak256(realm)
    )
  )
end

.uint256(value) ⇒ Object

Raises:

  • (ArgumentError)


109
110
111
112
113
114
# File 'lib/mpp/methods/tempo/proof.rb', line 109

def uint256(value)
  value = Integer(value)
  raise ArgumentError, "uint256 out of range" if value.negative? || value >= (1 << 256)

  [value.to_s(16).rjust(64, "0")].pack("H*")
end

.verify(address:, chain_id:, challenge_id:, realm:, signature:) ⇒ Object

Verify a proof credential signature (server-side).



65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/mpp/methods/tempo/proof.rb', line 65

def verify(address:, chain_id:, challenge_id:, realm:, signature:)
  Kernel.require "eth"

  hash = signing_hash(chain_id: chain_id, challenge_id: challenge_id, realm: realm)
  sig_bytes = [signature.delete_prefix("0x")].pack("H*")

  # Recover the signer address from the signature
  recovered = recover_address(hash, sig_bytes)
  return false unless recovered

  recovered.downcase == address.downcase
end