import assert from 'assert' ; import * as base32 from 'hi-base32' ; import { rlp } from 'ethereumjs-util' ; import { sscanf } from 'scanf' ; import { ecdsaVerify } from 'secp256k1' ; import { Multiaddr } from 'multiaddr' ; import base64url from 'base64url' ; import { PeerInfo } from '../dpt' ; import { toNewUint8Array , keccak256 } from '../util' ; const Convert = require ( 'multiaddr/src/convert' ) ; type ProtocolCodes = { ipCode : number ; tcpCode : number ; udpCode : number ; } ; type ENRRootValues = { eRoot : string ; lRoot : string ; seq : number ; signature : string ; } ; type ENRTreeValues = { publicKey : string ; domain : string ; } ; export class ENR { public static readonly RECORD_PREFIX = 'enr:' ; public static readonly TREE_PREFIX = 'enrtree:' ; public static readonly BRANCH_PREFIX = 'enrtree-branch:' ; public static readonly ROOT_PREFIX = 'enrtree-root:' ; param { string } enr return { PeerInfo } static parseAndVerifyRecord ( enr ) { assert ( enr . startsWith ( this . RECORD_PREFIX ) , `String encoded ENR must start with '${ this . RECORD_PREFIX }'` ) ; const base64BufferEnr = base64url . toBuffer ( enr . slice ( this . RECORD_PREFIX . length ) ) ; const decoded = rlp . decode ( base64BufferEnr ) as unknown as Buffer [ ] ; const [ signature , seq , ... kvs ] = decoded ; const obj = { } ; for ( let i = 0 ; i < kvs . length ; i += 2 ) { obj [ kvs [ i ] . toString ( ) ] = Buffer . from ( kvs [ i + 1 ] ) ; } const isVerified = ecdsaVerify ( signature , keccak256 ( rlp . encode ( [ seq , ... kvs ] ) ) , obj . secp256k1 ) ; assert ( isVerified , 'Unable to verify ENR signature' ) ; const { ipCode , tcpCode , udpCode } = this . _getIpProtocolConversionCodes ( obj . id ) ; const peerInfo = { address : Convert . toString ( ipCode , obj . ip ) as string , tcpPort : Number ( Convert . toString ( tcpCode , toNewUint8Array ( obj . tcp ) ) ) , udpPort : Number ( Convert . toString ( udpCode , toNewUint8Array ( obj . udp ) ) ) , } ; return peerInfo ; } param { string } root return { string } static parseAndVerifyRoot ( root , publicKey ) { assert ( root . startsWith ( this . ROOT_PREFIX ) , `ENR root entry must start with '${ this . ROOT_PREFIX }'` ) ; const rootVals = sscanf ( root , `${ this . ROOT_PREFIX }v1 e=%s l=%s seq=%d sig=%s` , 'eRoot' , 'lRoot' , 'seq' , 'signature' ) as ENRRootValues ; assert . ok ( rootVals . eRoot , "Could not parse 'e' value from ENR root entry" ) ; assert . ok ( rootVals . lRoot , "Could not parse 'l' value from ENR root entry" ) ; assert . ok ( rootVals . seq , "Could not parse 'seq' value from ENR root entry" ) ; assert . ok ( rootVals . signature , "Could not parse 'sig' value from ENR root entry" ) ; const decodedPublicKey = base32 . decode . asBytes ( publicKey ) ; const signedComponent = root . split ( ' sig' ) [ 0 ] ; const signedComponentBuffer = Buffer . from ( signedComponent ) ; const signatureBuffer = base64url . toBuffer ( rootVals . signature ) . slice ( 0 , 64 ) ; const keyBuffer = Buffer . from ( decodedPublicKey ) ; const isVerified = ecdsaVerify ( signatureBuffer , keccak256 ( signedComponentBuffer ) , keyBuffer ) ; assert ( isVerified , 'Unable to verify ENR root signature' ) ; return rootVals . eRoot ; } param { string } tree return { ENRTreeValues } static parseTree ( tree ) { assert ( tree . startsWith ( this . TREE_PREFIX ) , `ENR tree entry must start with '${ this . TREE_PREFIX }'` ) ; const treeVals = sscanf ( tree , `${ this . TREE_PREFIX }//%s@%s` , 'publicKey' , 'domain' ) as ENRTreeValues ; assert . ok ( treeVals . publicKey , 'Could not parse public key from ENR tree entry' ) ; assert . ok ( treeVals . domain , 'Could not parse domain from ENR tree entry' ) ; return treeVals ; } param { string } branch return { string [ ] } static parseBranch ( branch ) { assert ( branch . startsWith ( this . BRANCH_PREFIX ) , `ENR branch entry must start with '${ this . BRANCH_PREFIX }'` ) ; return branch . split ( this . BRANCH_PREFIX ) [ 1 ] . split ( ',' ) ; } param { Buffer } protocolId return { ProtocolCodes } static _getIpProtocolConversionCodes ( protocolId ) { let ipCode ; switch ( protocolId . toString ( ) ) { case 'v4' : ipCode = Multiaddr . protocols . names . ip4 . code ; break ; case 'v6' : ipCode = Multiaddr . protocols . names . ip6 . code ; break ; default : throw new Error ( "IP protocol must be 'v4' or 'v6'" ) ; } return { ipCode , tcpCode : Multiaddr . protocols . names . tcp . code , udpCode : Multiaddr . protocols . names . udp . code , } ; } }