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: “1”, chainId } Types: { Proof: [{ name: “challengeId”, type: “string” }] } Message: { challengeId: <challenge.id> }

Constant Summary collapse

DOMAIN_NAME =
"MPP"
DOMAIN_VERSION =
"1"
DOMAIN_TYPE_HASH =

EIP-712 domain separator type hash

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

Class Method Summary collapse

Class Method Details

.abi_encode(*values) ⇒ Object

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



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

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.



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

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



112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/mpp/methods/tempo/proof.rb', line 112

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:) ⇒ Object

Sign a proof credential (client-side).



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

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

.signing_hash(chain_id:, challenge_id:) ⇒ Object

Compute the full EIP-712 signing hash.



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

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

.source(address:, chain_id:) ⇒ Object

Construct source DID for proof credentials.



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

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

.struct_hash(challenge_id) ⇒ Object

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



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

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

.uint256(value) ⇒ Object



108
109
110
# File 'lib/mpp/methods/tempo/proof.rb', line 108

def uint256(value)
  [value].pack("Q>").rjust(32, "\x00".b)
end

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

Verify a proof credential signature (server-side).



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

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

  hash = signing_hash(chain_id: chain_id, challenge_id: challenge_id)
  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