Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions servers/cu/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions servers/cu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
},
"dependencies": {
"@fastify/middie": "^9.0.3",
"@noble/ed25519": "^2.1.0",
"@permaweb/ao-loader": "^0.0.44",
"@permaweb/ao-scheduler-utils": "^0.0.25",
"@permaweb/weavedrive": "^0.0.18",
"arweave": "^1.15.5",
"async-lock": "^1.4.1",
"better-sqlite3": "^11.8.0",
"bs58": "^6.0.0",
"bytes": "^3.1.2",
"cors": "^2.8.5",
"dataloader": "^2.2.3",
Expand Down
51 changes: 47 additions & 4 deletions servers/cu/src/domain/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LRUCache } from 'lru-cache'
import createKeccakHash from 'keccak'
import bs58 from 'bs58'
import { Rejected, Resolved } from 'hyper-async'
import {
F, T, __, allPass, always, append, assoc, chain, concat, cond, defaultTo, equals,
Expand Down Expand Up @@ -483,6 +484,16 @@ const SIG_CONFIG = {
sigLength: 65,
pubLength: 65,
sigName: 'ethereum'
},
ED25519: {
sigLength: 64,
pubLength: 32,
sigName: 'ed25519'
},
SOLANA: {
sigLength: 64,
pubLength: 32,
sigName: 'solana'
}
}

Expand Down Expand Up @@ -525,6 +536,34 @@ function keyToEthereumAddress (key) {
return checksumAddress
}

/**
* Solana addresses are simply the base58-encoded Ed25519 public key.
* Unlike Ethereum which hashes the public key, Solana uses the raw public key.
*
* See: https://docs.solana.com/terminology#public-key-pubkey
*
* @param {string} key - base64url encoded 32-byte Ed25519 public key
* @returns {string} - base58 encoded Solana address
*/
function keyToSolanaAddress (key) {
/**
* Decode the base64url encoded public key to raw bytes
*/
const pubKeyBytes = Buffer.from(key, 'base64url')

/**
* Validate it's exactly 32 bytes (Ed25519 public key length)
*/
if (pubKeyBytes.length !== 32) {
throw new Error(`Invalid Ed25519 public key length: expected 32, got ${pubKeyBytes.length}`)
}

/**
* Solana address is the base58-encoded public key
*/
return bs58.encode(pubKeyBytes)
}

const cache = new LRUCache({ max: 1000 })
/**
* Given the address and public key, generate the owner.
Expand All @@ -542,11 +581,15 @@ export const addressFrom = ({ address, key }) => {
let _address

if (pubKeyLength === SIG_CONFIG.ETHEREUM.pubLength) _address = keyToEthereumAddress(key)
else if (pubKeyLength === SIG_CONFIG.ARWEAVE.pubLength) _address = address
else if (pubKeyLength === SIG_CONFIG.ED25519.pubLength || pubKeyLength === SIG_CONFIG.SOLANA.pubLength) {
/**
* Both Ed25519 (Type 2) and Solana (Type 4) use 32-byte keys
* and derive addresses using base58 encoding
*/
_address = keyToSolanaAddress(key)
} else if (pubKeyLength === SIG_CONFIG.ARWEAVE.pubLength) _address = address
/**
* Assume the address is the owner
*
* TODO: implement bonafide logic for other signers
* Unknown signature type, assume the address is the owner
*/
else _address = address

Expand Down
105 changes: 105 additions & 0 deletions servers/cu/src/domain/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,5 +450,110 @@ describe('utils', () => {
assert.equal(res.length, 42) // last 20 bytes prefixed with '0x'
})
})

test('should return the solana address for solana/ed25519 public keys', () => {
const address = 'some-arweave-transaction-id-1234567890'
/**
* Test with a 32-byte Ed25519 public key (base64url encoded)
* This represents a typical Solana wallet public key
*/
const key = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'

const res = addressFrom({ address, key })

/**
* Should derive Solana address, not return the Arweave tx ID
*/
assert.notEqual(res, address, 'Should not return Arweave address for Ed25519 key')

/**
* Solana addresses use base58 encoding (no 0, O, I, l characters)
*/
assert.ok(/^[1-9A-HJ-NP-Za-km-z]+$/.test(res), 'Should be valid base58 encoding')

/**
* Solana addresses are typically 32-44 characters (base58 encoding of 32 bytes)
*/
assert.ok(res.length >= 32 && res.length <= 44, `Address length should be 32-44, got ${res.length}`)

/**
* All-zero key is Solana's system program address (known value)
*/
assert.equal(res, '11111111111111111111111111111111', 'All-zero key should produce system program address')
})

test('should return consistent solana addresses for same key', () => {
/**
* Create a test Ed25519 key (32 bytes of sequential data)
*/
const testKeyBytes = Buffer.from([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32
])
const key = testKeyBytes.toString('base64url')
const address = 'some-tx-id'

const res1 = addressFrom({ address, key })
const res2 = addressFrom({ address, key })

/**
* Same key should always produce same address (testing caching)
*/
assert.strictEqual(res1, res2, 'Same key should always produce same address')

/**
* Should be valid base58
*/
assert.ok(/^[1-9A-HJ-NP-Za-km-z]+$/.test(res1), 'Should be valid base58 encoding')
})

test('should differentiate between ethereum and solana keys by length', () => {
/**
* Ed25519 key (32 bytes) vs Ethereum uncompressed key (65 bytes)
*/
const ed25519Key = Buffer.alloc(32, 1).toString('base64url')
const ethereumKey = Buffer.alloc(65, 1).toString('base64url')
const address = 'some-tx-id'

const solanaAddr = addressFrom({ address, key: ed25519Key })
const ethAddr = addressFrom({ address, key: ethereumKey })

/**
* Solana addresses are base58, Ethereum addresses start with 0x
*/
assert.ok(!solanaAddr.startsWith('0x'), 'Solana address should not start with 0x')
assert.ok(ethAddr.startsWith('0x'), 'Ethereum address should start with 0x')

/**
* Should produce different address formats
*/
assert.notEqual(solanaAddr, ethAddr, 'Should produce different address formats')

/**
* Ethereum addresses are always 42 characters (0x + 40 hex)
*/
assert.equal(ethAddr.length, 42, 'Ethereum address should be 42 characters')

/**
* Solana addresses are typically 32-44 characters (base58)
*/
assert.ok(solanaAddr.length >= 32 && solanaAddr.length <= 44, 'Solana address should be 32-44 characters')
})

test('should fallback to address for unknown key lengths', () => {
/**
* Test with wrong key lengths (not 32, 65, or 512 bytes)
* Following CU pattern: unknown types fall through and return address
*/
const invalidKey = Buffer.alloc(31).toString('base64url') // 31 bytes, not a known type
const address = 'some-tx-id'

/**
* Should fall through to default behavior and return address
* This matches the existing CU pattern for unknown signature types
*/
const res = addressFrom({ address, key: invalidKey })
assert.equal(res, address, 'Unknown key lengths should return the provided address')
})
})
})
Loading